Extending with Custom Storage Adapters
Relevant source files
The following files were used as context for generating this wiki page:Gr8hopper is designed for portability, capable of running on edge platforms like Cloudflare Workers or traditional VPS environments using Node.js. This flexibility is achieved through a storage adapter pattern that abstracts the persistence layer for routes and settings. By default, Gr8hopper includes adapters for Cloudflare KV and local JSON files, but the system can be extended to support any database or storage backend by implementing a custom storage adapter.
This document outlines the StorageAdapter interface, details the built-in adapters, and provides a guide for creating and integrating a new custom adapter into the application. This allows developers to connect Gr8hopper to backends like PostgreSQL, Redis, or other cloud-based data stores.
Sources: CONTRIBUTING.md:43-45, README.md:275-283
The StorageAdapter Interface
The StorageAdapter interface defines the contract for all data persistence operations within Gr8hopper. Any class that correctly implements this interface can be used as the application's storage backend. The request handlers for both redirects and the admin panel interact exclusively with this interface, making them completely decoupled from the underlying storage technology.
Sources: src/storage/json-file.ts:5, test/admin.test.ts:6
Architecture Overview
The following diagram illustrates how the StorageAdapter fits into the application architecture. The entry points (server.ts or index.ts) instantiate a concrete adapter and inject it into the request handlers.
This diagram shows the dependency injection of a storage adapter into the application handlers.
Sources: src/server.ts:133, src/index.ts:31, src/handlers/admin.ts:18
Class Diagram
The StorageAdapter interface and its known implementations can be visualized as follows.
This diagram shows the StorageAdapter interface and the classes that implement it.
Sources: src/storage/json-file.ts:8, src/index.ts:31, test/admin.test.ts:6
Interface Methods
A custom adapter must implement the following methods to be compatible with Gr8hopper's handlers.
| Method | Parameters | Returns | Description |
|---|---|---|---|
getRoute | id: string | Promise<RouteConfig | null> | Retrieves a single route configuration by its ID. |
getAllRoutes | (none) | Promise<StoredRoute[]> | Fetches all routes from storage. StoredRoute includes the id. |
getPatternRoutes | (none) | Promise<StoredRoute[]> | Fetches only routes that are identified as patterns (containing *, {, etc.). |
setRoute | id: string, config: RouteConfig | Promise<void> | Creates or updates a single route. |
deleteRoute | id: string | Promise<boolean> | Deletes a route by its ID. Returns true if successful. |
getSettings | (none) | Promise<GlobalSettings> | Retrieves the global application settings. |
setSettings | settings: GlobalSettings | Promise<void> | Updates the global application settings. |
setRoutes | routes: Array<{ id, config }>, clearExisting: boolean | Promise<void> | Bulk-sets multiple routes. If clearExisting is true, it replaces the entire route set. Used by the import feature. |
deleteRoutes | ids: string[] | Promise<void> | Bulk-deletes multiple routes by their IDs. |
Sources: src/storage/json-file.ts, test/admin.test.ts
Built-in Adapters
Gr8hopper provides two concrete implementations of StorageAdapter out of the box.
JsonFileAdapter (for Node.js/VPS)
This adapter is used for VPS deployments running on Node.js or Bun. It persists all routes and settings to a single JSON file.
- Initialization: The adapter is instantiated with a file path. An
init()method reads the file, parses the JSON, and creates the file with default settings if it doesn't exist. - Persistence: Any write operation (
setRoute,setSettings, etc.) triggers apersist()method, which serializes the in-memory data object back to the JSON file. - Configuration: The file path is configured via the
CONFIG_FILEenvironment variable, defaulting to./routes.json.
Sources: src/server.ts:86, 133, src/storage/json-file.ts:12, 20-33, 51-54
// File: src/server.ts:133-134
// Create storage adapter
const storage = new JsonFileAdapter(CONFIG_FILE)
KVAdapter (for Cloudflare Workers)
This adapter is used for edge deployments on Cloudflare Workers. It leverages Cloudflare's globally distributed key-value store, KV.
- Initialization: The adapter is instantiated with a
KVNamespacebinding, which is configured inwrangler.tomland made available on the environment object (c.env.ROUTES_KV). - Persistence: Each route and the global settings object are stored as separate keys in the KV namespace. This approach is more granular and suited to KV's key-value model.
- Configuration: The KV namespace binding is named
ROUTES_KVby convention.
Sources: src/index.ts:28-31, README.md:275-277
// File: src/index.ts:28-31
if (!env.ROUTES_KV) {
return c.text('ROUTES_KV binding not configured', 500)
}
const storage = new KVAdapter(env.ROUTES_KV)
Creating a Custom Adapter
The process for adding a new storage adapter involves implementing the interface and integrating it into an application entry point.
Sources: CONTRIBUTING.md:43-45
Integration Flow
The following flowchart outlines the steps to create and use a new storage adapter.
This diagram shows the development workflow for adding a custom storage adapter.
Step 1: Implement the StorageAdapter Interface
Create a new file in src/storage/ and define a class that implements StorageAdapter. A minimal in-memory implementation for testing purposes can be found in test/admin.test.ts.
// File: test/admin.test.ts:6-45
class FakeStorage implements StorageAdapter {
private routes = new Map<string, RouteConfig>()
private settings: GlobalSettings = {
fallback_url: '/not-found',
cache_ttl: 60,
route_param: 'r'
}
async getRoute(id: string): Promise<RouteConfig | null> {
return this.routes.get(id) || null
}
async getAllRoutes(): Promise<StoredRoute[]> {
return Array.from(this.routes.entries()).map(([id, config]) => ({ ...config, id }))
}
async setRoute(id: string, config: RouteConfig): Promise<void> {
this.routes.set(id, config)
}
async deleteRoute(id: string): Promise<boolean> {
return this.routes.delete(id)
}
async getSettings(): Promise<GlobalSettings> {
return this.settings
}
async setSettings(settings: GlobalSettings): Promise<void> {
this.settings = settings
}
async setRoutes(routes: Array<{ id: string; config: RouteConfig }>, clearExisting = false): Promise<void> {
if (clearExisting) {
this.routes.clear()
}
for (const { id, config } of routes) {
this.routes.set(id, config)
}
}
async deleteRoutes(ids: string[]): Promise<void> {
for (const id of ids) {
this.routes.delete(id)
}
}
}
This FakeStorage class provides a simple, complete implementation of the StorageAdapter interface using an in-memory Map, serving as a useful template.
Sources: test/admin.test.ts:6-45
Step 2: Integrate into an Entry Point
Modify the relevant entry point (src/server.ts for Node.js or src/index.ts for Cloudflare Workers) to use your new adapter. You will replace the instantiation of JsonFileAdapter or KVAdapter with your own.
Example for src/server.ts:
// import { YourCustomAdapter } from './storage/your-custom-adapter.js'
// ...
// Create storage adapter
// const storage = new JsonFileAdapter(CONFIG_FILE) // Old
const storage = new YourCustomAdapter(/* connection options */) // New
// ...
// Create handlers
const redirectHandler = createRedirectHandler({ storage })
const adminHandler = createAdminHandler({ storage, auth: authConfig })
Sources: src/server.ts:133, 145-146, CONTRIBUTING.md:45
How Adapters Are Used
The storage adapter instance is passed as an option to the createAdminHandler and createRedirectHandler factory functions. These handlers then use the adapter to perform all data operations, such as fetching a route for a redirect or listing all routes in the admin panel.
Admin API Request Flow
This sequence diagram shows how a GET /admin/routes request flows through the system to retrieve data via the storage adapter.
This diagram shows the adminHandler calling the getAllRoutes method on the injected storage adapter to fulfill an API request.
Sources: src/handlers/admin.ts:70-75, src/server.ts:146
Conclusion
The StorageAdapter pattern is a key architectural feature of Gr8hopper, ensuring the application remains decoupled from its data persistence layer. This design not only facilitates its dual-platform support for Cloudflare Workers and Node.js but also empowers developers to extend its capabilities. By implementing a single interface, Gr8hopper can be adapted to work with a wide variety of databases and storage services, making it a highly versatile and extensible redirect management solution.
Sources: CONTRIBUTING.md:43-45, README.md:275-283