Keelv0.86.0
Docs / Hosting

Keel Hosting is a small toolkit for hosted Workers / D1 apps: a Cloudflare REST client, hostname helpers, a SQLite-compatible SQL dump, and purpose-scoped secret encryption. It ships as @shaferllc/keel/hosting.

This is infrastructure — not a control plane. Site orchestration, plans, and deploy loops live in your app (for example Keel Cloud).

Install

import {
  CloudflareClient,
  cloudflareConfigured,
  normalizeHostname,
  isValidHostname,
  zoneCandidates,
  dumpConnection,
  normalizeSecretKey,
  encryptSecretValue,
  decryptSecretValue,
  resolveSecretRows,
} from "@shaferllc/keel/hosting";

No service provider — import what you need.

Cloudflare

const creds = {
  accountId: process.env.CF_ACCOUNT_ID!,
  apiToken: process.env.CF_API_TOKEN!,
};

if (!cloudflareConfigured(creds)) {
  throw new Error("Cloudflare credentials missing");
}

const cf = new CloudflareClient(creds);
const db = await cf.createD1Database("kc-acme");

Credentials are constructor args — no app config coupling. Optional pinnedZoneId / pinnedZoneName skip a zone lookup when you already know the primary zone.

Hostnames

const host = normalizeHostname("https://App.Example.com/"); // "app.example.com"
isValidHostname(host); // true
zoneCandidates(host);  // ["app.example.com", "example.com"]

zoneCandidates walks from most-specific to apex — useful when attaching a Workers Custom Domain and you need to find which zone owns the name.

SQL dump

Dump any SQLite-compatible Connection to a portable .sql script (schema + data). Useful for export / escape hatches:

import { db } from "@shaferllc/keel/core";
import { dumpConnection } from "@shaferllc/keel/hosting";

const sql = await dumpConnection(db(), "Acme local D1", { generatedBy: "Keel Cloud" });
// write sql to a .sql file; restore with sqlite3 / D1 import

Secrets

Encrypt vault values with Keel's purpose-scoped encryption (config('app.key') must be set). Keys are normalized to ENV_STYLE identifiers:

const key = normalizeSecretKey("stripe-secret-key"); // "STRIPE_SECRET_KEY"
const encrypted = await encryptSecretValue(secret, "app-secret");
const plain = await decryptSecretValue(encrypted, "app-secret");

const env = await resolveSecretRows(
  [{ key: "STRIPE_SECRET_KEY", value_encrypted: encrypted }],
  "app-secret",
);
// { STRIPE_SECRET_KEY: "…" }

Your app owns the table of rows (owner_id, key, value_encrypted); hosting only encrypts and decrypts.

  • Gates — private-alpha signup gating used by hosted control planes
  • Starter kits — presets Cloud scaffolds from
  • Building with AI — MCP Cloud tools (keel_cloud_*) that drive hosting