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.

GoalFiles to touchEffort
Credential type onlypackages/node-types/src/credentials.ts1 line
+ SSRF / tunnel routingapi/src/common/ssrf-guard.ts~50 lines (parseFirst*Host + rewrite*UriHost)
Action node (CRUD)api/src/nodes/<name>-actions/ + dashboard mirrorFull new module — see postgres-actions/
Vector backendapi/src/vector-stores/backends/<name>.backend.ts + registry case1 backend file + 5 lines registry
Memory backendapi/src/nodes/ai-nodes/memory/<name>.backend.ts1 backend file (self-registers)
Reference implementations. Postgres support landed across all five surfaces in early 2026 — read those PRs as a working example. The shape is intentionally identical to Mongo's path so a third DB (Redis, MySQL) just copies one of the two and adapts the dialect.

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.

typescriptpackages/node-types/src/credentials.ts
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:

typescriptapi/src/common/ssrf-guard.ts
/**
 * 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.
Strip TLS params on rewrite. The forwarder is plaintext; the agent restores TLS to the real host on its side. Leaving 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.

  1. Create api/src/vector-stores/backends/<name>.backend.ts implementing the interface. Use pgvector.backend.ts as the BYO reference and mongo-atlas.backend.ts as the managed reference.
  2. Export a BackendMetadata object with the UI config schema (label, description, configSchema fields).
  3. Add a case to backends/registry.ts's switch + the metadata to listBackendMetadata().
  4. Optional: add a test-connection branch in vector-stores.service.ts's testBackendConnection for the BackendPicker's button.
  5. The dashboard's BackendPicker auto-renders the new entry from the catalog API. Add the provider name to TESTABLE_BACKENDS in 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.

typescriptapi/src/nodes/ai-nodes/memory/<name>.backend.ts (skeleton)
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.

Per-backend config sections in the memory tab stay hand-coded because each backend's form differs (credential picker scope, manual mode placeholder, table vs key-prefix label). Keep the new section conditional on locals.memoryBackend === 'redis'.

Final checklist

  • npx tsc --noEmit clean 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_ERROR fallback.
  • 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>-action page (action node) or app/vector-stores/bring-your-own-<name> (vector backend) modeled on the postgres-action / pgvector docs. Sidebar entry + search-index keyword bump.