Storage Adapters
Relevant source files
The following files were used as context for generating this wiki page:The Storage Adapter system in gr8hopper provides a crucial abstraction layer for data persistence. It decouples the core application logic from the underlying storage mechanism, enabling the application to run on different platforms with distinct storage solutions. This design is central to gr8hopper's portability, allowing it to be deployed on both Cloudflare Workers (using Cloudflare KV) and traditional VPS environments (using a local JSON file).
The entire system is built around the StorageAdapter interface, which defines a contract for all data operations, such as managing routes and global settings. The application's entry points (src/index.ts for Cloudflare, src/server.ts for Node.js) are responsible for instantiating the appropriate adapter and injecting it into the request handlers. This ensures that the redirect and admin logic remain agnostic of where or how the data is stored.
Sources: README.md, src/storage/adapter.ts, src/server.ts:133, src/index.ts:31
The StorageAdapter Interface
The StorageAdapter interface is the cornerstone of the storage system. It defines a standard set of methods that any storage implementation must provide to be compatible with gr8hopper.
Sources: src/storage/adapter.ts:8-33
Interface Definition
This diagram shows the StorageAdapter interface and its concrete implementations for different deployment environments.
Methods
The following table details the methods defined by the StorageAdapter interface.
| Method | Description |
|---|---|
getRoute(id: string) | Retrieves a single route configuration by its ID. |
getAllRoutes() | Retrieves all routes as an array of StoredRoute objects. |
getPatternRoutes() | Retrieves only the routes whose IDs are patterns (containing *, {, etc.). |
setRoute(id: string, config: RouteConfig) | Creates or updates a single route. |
deleteRoute(id: string) | Deletes a route by its ID. Returns true if successful. |
getSettings() | Retrieves the global application settings. |
setSettings(settings: GlobalSettings) | Updates the global application settings. |
setRoutes(routes, clearExisting) | Performs a bulk import of routes. If clearExisting is true, all current routes are deleted first. |
deleteRoutes(ids: string[]) | Deletes multiple routes in a single operation. |
Sources: src/storage/adapter.ts:9-32
Implementations
gr8hopper includes two primary storage adapter implementations, one for VPS/Node.js deployments and another for Cloudflare Workers.
JsonFileAdapter (VPS / Node.js)
This adapter is designed for self-hosted environments running on Node.js or Bun. It persists all routes and settings to a single JSON file.
- File Path: The location of the JSON file is determined by the
CONFIG_FILEenvironment variable, defaulting to./routes.json. - Initialization: The
init()method is called on server startup. If the config file does not exist, it creates a new one with default settings to ensure the application can start. - Persistence: All write operations (e.g.,
setRoute,setSettings) trigger apersist()call, which saves the entire in-memory data object back to the JSON file. ThesaveFilemethod ensures the directory exists before writing.
Sources: src/server.ts:80, 133, src/storage/json-file.ts
The following diagram illustrates the file-saving process for the JsonFileAdapter.
Sources: src/storage/json-file.ts:44-53
KVAdapter (Cloudflare Workers)
This adapter is optimized for the Cloudflare Workers serverless environment, leveraging the globally distributed Cloudflare KV store.
- KV Namespace: It requires a KV namespace binding named
ROUTES_KVto be configured inwrangler.toml. - Data Model:
- Routes: Each route is stored as a separate key-value pair with the prefix
routes:. For example, a route with IDmy-routeis stored under the keyroutes:my-route. - Settings: Global settings are stored under a single, static key:
settings.
- Routes: Each route is stored as a separate key-value pair with the prefix
- Operations:
getAllRoutesandgetPatternRoutesusekv.list({ prefix: '...' })to efficiently retrieve keys.- Bulk operations like
setRoutesanddeleteRoutesusePromise.allto execute multiple KV writes/deletes concurrently, which is more performant than sequential operations.
Sources: src/index.ts:29, src/storage/kv.ts
This sequence diagram shows how a new route is saved using the KVAdapter.
Sources: src/storage/kv.ts:33-35
Data Structures
The adapters are responsible for managing two primary data structures: RouteConfig and GlobalSettings.
RouteConfig
This object contains the configuration for a single redirect rule.
| Field | Type | Description |
|---|---|---|
template | string | The target URL, which can include {param} placeholders. |
active | boolean | A flag to enable or disable the route. |
passthrough | boolean (optional) | If true, query parameters from the source URL are appended to the destination URL. Defaults to false. |
Sources: src/storage/adapter.ts:4-7
GlobalSettings
This object contains application-wide settings.
| Field | Type | Default Value | Description |
|---|---|---|---|
fallback_url | string | /not-found | The URL to redirect to when no route is matched. |
cache_ttl | number | 604800 | The browser cache duration in seconds (1 week). |
route_param | string | r | The query parameter used for simple route selection (e.g., ?r=my-route). |
Sources: src/storage/adapter.ts:35-39
Import/Export Format
The admin panel's import/export feature uses a specific JSON structure that represents the entire application state. This format is also used for bulk-loading routes via scripts.
{
"routes": {
"my-route": {
"template": "https://example.com/product/{id}",
"active": true,
"passthrough": true
},
"another-route": {
"template": "https://partner.com/{category}/{id}?ref={route}",
"active": true
}
},
"settings": {
"fallback_url": "https://example.com/not-found",
"cache_ttl": 604800,
"route_param": "r"
}
}
Sources: README.md, src/handlers/admin.ts:213-224
Integration and Usage
The application's entry points are responsible for selecting and initializing the correct storage adapter based on the deployment environment. The adapter instance is then injected into the request handlers.
This diagram shows the dependency injection flow at application startup.
Sources: src/index.ts:26-47, src/server.ts:133-142
Extensibility
Adding a new storage backend (e.g., for a relational database like PostgreSQL or a different key-value store like Redis) is straightforward.
- Create a new class in the
src/storage/directory. - Implement the
StorageAdapterinterface, providing logic for all its methods. - Modify the relevant entry point (
src/server.tsor a new one) to instantiate and inject your new adapter, potentially based on an environment variable.
Sources: CONTRIBUTING.md