Adding a new DB type
How to wire a brand-new database (Redis, MySQL, MariaDB, …) into HostWebhook end-to-end. Each surface has a deliberate registry pattern; the typical cost is 1-2 files per surface, no scattered edits across the codebase.
The five surfaces
A new DB might land on as few as one surface (just a credential type) or all five (credential, action node, vector backend, memory backend, tunnel routing). Pick the ones you need; skip the rest.
| Goal | Files to touch | Effort |
|---|---|---|
| Credential type only | packages/node-types/src/credentials.ts | 1 line |
| + SSRF / tunnel routing | api/src/common/ssrf-guard.ts | ~50 lines (parseFirst*Host + rewrite*UriHost) |
| Action node (CRUD) | api/src/nodes/<name>-actions/ + dashboard mirror | Full new module — see postgres-actions/ |
| Vector backend | api/src/vector-stores/backends/<name>.backend.ts + registry case | 1 backend file + 5 lines registry |
| Memory backend | api/src/nodes/ai-nodes/memory/<name>.backend.ts | 1 backend file (self-registers) |
Step 1 — Register the credential type
The credential type registry is the single source of truth for "what DBs does HW accept credentials for". Both the Mongoose schema enum and the class-validator DTO pull from this list.
export const CREDENTIAL_TYPES: readonly CredentialTypeRegistration[] = [
// …existing entries…
{ type: 'redis', ssrfValidated: true, tunnelable: true },
];Bump @hostwebhook/node-types minor version, publish to npm, then npm install @hostwebhook/node-types@latest in the api + dashboard. That's it — the credential type is now selectable in the dashboard's New Credential page (after you add a sidebar entry + form block) and accepted by the api's create-credential DTO.
Step 2 — Tunnel routing (when applicable)
For credentials marked tunnelable: true, add two helpers to ssrf-guard parallel to the Mongo / Postgres ones:
/**
* Parse the first host:port pair out of a redis:// URI.
*/
export function parseFirstRedisHost(uri: string): { host: string; port: number } {
const match = /^redis(?:s)?:\/\/(?:[^@/]*@)?([^/?]+)/i.exec(uri);
if (!match) throw new Error('Connection string is not a valid redis:// URI');
const firstHost = match[1].trim().replace(/^\[/, '').replace(/\].*$/, '');
const portIdx = firstHost.lastIndexOf(':');
if (portIdx > 0 && /^\d+$/.test(firstHost.slice(portIdx + 1))) {
return { host: firstHost.slice(0, portIdx), port: Number(firstHost.slice(portIdx + 1)) };
}
return { host: firstHost, port: 6379 };
}
/**
* Replace host:port of a redis URI with the local forwarder address.
* Drop TLS-related params — local hop is plaintext, agent restores TLS.
*/
export function rewriteRedisUriHost(uri: string, host: string, port: number): string {
const m = /^redis(?:s)?:\/\/(?:([^@/]*)@)?([^/?]+)(\/[^?]*)?(\?.*)?$/i.exec(uri);
if (!m) throw new Error('Connection string is not a valid redis:// URI');
const userInfo = m[1];
const path = m[3] ?? '';
let query = m[4] ?? '';
if (query) {
const params = new URLSearchParams(query.slice(1));
for (const k of ['tls', 'ssl', 'sslca']) params.delete(k);
const remaining = params.toString();
query = remaining ? `?${remaining}` : '';
}
const auth = userInfo ? `${userInfo}@` : '';
return `redis://${auth}${host}:${port}${path}${query}`;
}
// Plus assertSafeRedisUri / assertSafeRedisUriUnlessTunneled
// matching the Mongo / Postgres pair. sslmode=require (or equivalents) in the rewritten URI causes the driver to negotiate TLS against the local listener and fail.Step 3 — Action node
Create api/src/nodes/<name>-actions/ following the postgres-actions layout: entity, service, controller, DTO, module. The shape is consistent across action nodes so once you've copied + adapted, the registration with the canvas dispatch happens via onModuleInit() → registerNodeHandler(...) in the service.
Don't forget the ten parallel touch-points from the new-node checklist (CREDENTIAL_TYPES already covered above; node-types registry, dashboard list/create/detail pages, FlowPanel / FlowCanvas / data slice / hooks). The reference for that list lives in the tribal-knowledge memory note — ask Claude when you're starting a new node.
Step 4 — Vector backend (optional)
Implement VectorBackend from api/src/vector-stores/backends/backend.ts. The interface is genuinely DB-agnostic (insert / query / delete / count / listChunks / findExistingHashes); the work is dialect translation.
- Create
api/src/vector-stores/backends/<name>.backend.tsimplementing the interface. Usepgvector.backend.tsas the BYO reference andmongo-atlas.backend.tsas the managed reference. - Export a
BackendMetadataobject with the UI config schema (label, description, configSchema fields). - Add a case to
backends/registry.ts's switch + the metadata tolistBackendMetadata(). - Optional: add a test-connection branch in
vector-stores.service.ts'stestBackendConnectionfor the BackendPicker's button. - The dashboard's
BackendPickerauto-renders the new entry from the catalog API. Add the provider name toTESTABLE_BACKENDSin BackendPicker.tsx if you support test-connection.
Step 5 — Memory backend (optional)
Implement MemoryBackend (recall + ingest) plus the static admin shape from memory/registry.ts. Everything else flows through the registry — no edits in the service or controller.
import {
registerMemoryBackend,
type MemoryAdminParams,
type MemoryBackendStaticAdmin,
} from './registry';
import type { MemoryBackend } from './memory-backend';
export class RedisMemoryBackend implements MemoryBackend {
// recall + ingest — your impl
async recall(query, userId, limit) { /* … */ }
async ingest(userId, messages) { /* … */ }
// Static admin surface (listSessions / listRecords / delete*)
static async listSessions(p: MemoryAdminParams) { /* … */ }
// …
}
const adminStatic: MemoryBackendStaticAdmin = {
listSessions: (p) => RedisMemoryBackend.listSessions(p),
// …
};
registerMemoryBackend({
catalog: {
name: 'redis',
label: 'External Redis',
description: 'Bring your own Redis. Ephemeral memory.',
managed: false,
semantic: false,
configSchema: [
{ key: 'memoryCredentialId', label: 'Redis Credential', type: 'credential', credentialTypes: ['redis'], required: true },
{ key: 'memoryCollectionName', label: 'Key prefix', type: 'text', placeholder: 'ai_memories', default: 'ai_memories' },
],
},
resolveConfig: async (entity, deps) => { /* credential → config */ },
resolveAdminParams: async (entity, deps) => { /* credential → admin params */ },
factory: async (config, deps) => RedisMemoryBackend.getOrCreate(/* … */),
adminStatic,
});Add a side-effect import to api/src/nodes/ai-nodes/ai-nodes.module.ts so the registration fires at boot. The dashboard memory tab's button list auto-includes the new backend (data-driven from the catalog webhook). Optionally add an icon entry to MEMORY_BACKEND_ICONS in the AI node detail page; otherwise it falls back to the generic 💾 emoji.
locals.memoryBackend === 'redis'.Final checklist
npx tsc --noEmitclean on api + dashboard.- Local smoke: create a credential of the new type, run the relevant flow (action node test, vector store ingest + query, AI memory ingest + recall), check Railway logs for the expected source tag.
- Telemetry: confirm runs surface in the dashboard's telemetry feed with the right source label (e.g.
redis_action) and not the generic*_action_ERRORfallback. - Tunnel test (if tunnelable): expose-tcp against a private host, confirm the credential routes through the local forwarder via the "tunneled via" log line.
- Documentation: add a
app/nodes/<name>-actionpage (action node) orapp/vector-stores/bring-your-own-<name>(vector backend) modeled on the postgres-action / pgvector docs. Sidebar entry + search-index keyword bump.