v0.1.0 Latest

Cloudflare

The container behind a Cloudflare Tunnel, the public knowledge base on its own hostname, or the whole app as a Worker.

Two ways, and they are not the same thing:

  • The container behind a Cloudflare Tunnel. Trove KB keeps running in Docker on your own host; Cloudflare publishes it with no port open to the internet. Start here.
  • A Cloudflare Worker. No server at all, but the database moves to Hyperdrive, attachments to R2, and the vault sidecar needs a tunnel of its own.

Both run the same code against the same schema, and password hashes are interchangeable between them.

Behind a Cloudflare Tunnel

browser -> Cloudflare edge -> tunnel -> cloudflared (this host) -> 127.0.0.1:3080 -> container
  1. Point APP_URL at the hostname people will type, for example https://trove-kb.yourdomain.com. Session cookies are Secure as soon as it is https, the OAuth state check is built from it, and every absolute link comes from it. A mismatch shows up as a sign-in page that loops, or as state_mismatch on single sign-on.

  2. Publish the hostname. Zero Trust → Networks → Tunnels → your tunnel → Public Hostname → Add: subdomain trove-kb, type HTTP, URL 127.0.0.1:3080, HTTP Host Header left empty. Or in /etc/cloudflared/config.yml:

    ingress:
      - hostname: trove-kb.yourdomain.com
        service: http://127.0.0.1:3080
      - service: http_status:404
  3. Close the port to everything else: APP_BIND=127.0.0.1, then docker compose up -d.

  4. Check it end to end from somewhere that is not this host: curl https://trove-kb.yourdomain.com/api/health answers {"status":"ok"}.

Uploads pass through Cloudflare, which caps a request body at 100 MB on free and Pro plans; keep MAX_UPLOAD_MB under it. Cloudflare Access can sit in front for a second gate; leave /api/* out of the policy or scope it to a service token, since an API or MCP client has nowhere to put an Access login page. Single sign-on redirect URIs move with the hostname: https://trove-kb.yourdomain.com/api/auth/callback/oidc.

Publishing the public knowledge base

The public knowledge base (/pub/kb) belongs on a hostname of its own, such as kb.yourdomain.com, behind a proxy that passes the knowledge base and nothing else:

server {
    listen 80;
    server_name kb.yourdomain.com;

    proxy_set_header X-Real-IP       $http_cf_connecting_ip;
    proxy_set_header X-Forwarded-For $http_cf_connecting_ip;
    proxy_set_header Host            $host;
    proxy_set_header X-Forwarded-Proto https;
    proxy_set_header Cookie "";
    proxy_hide_header Set-Cookie;

    location = /                 { return 302 https://$host/pub/kb; }
    location ^~ /pub/kb          { proxy_pass http://127.0.0.1:3080; }
    location ^~ /_next/static/   { proxy_pass http://127.0.0.1:3080; }
    location = /api/branding/logo { proxy_pass http://127.0.0.1:3080; }
    location = /favicon.ico      { proxy_pass http://127.0.0.1:3080; }
    location /                   { return 404; }
}

Publish kb.yourdomain.com on the tunnel to http://localhost:80. In Trove KB, under Admin → Settings → Public knowledge base, choose who is admitted; in Zero Trust, add a self-hosted Access application for the hostname with a Bypass policy for the same address ranges. Each collection has Show on the public site, off by default.

Running as a Worker

PieceWhy
A Postgres databaseNeon, Supabase, RDS, anything Hyperdrive can reach
HyperdrivePools connections at the edge; a Worker cannot hold one open
An R2 bucketAttachments. Workers have no filesystem
Workers paid planArgon2id costs a few hundred milliseconds of CPU per sign-in
npx wrangler hyperdrive create trove-kb --connection-string="postgres://…"
npx wrangler r2 bucket create trove-kb-uploads
# put the Hyperdrive id into wrangler.jsonc
npx wrangler secret put AUTH_SECRET
npx wrangler secret put APP_URL
npx wrangler secret put CRON_SECRET
DATABASE_URL="postgres://…" npm run db:migrate
DATABASE_URL="postgres://…" npm run db:seed    # optional
npm run cf:deploy

What differs from the container: webhook retries and schedule passes come from a Cron Trigger calling /api/internal/webhooks with CRON_SECRET; attachments go to R2 through the TROVE_UPLOADS binding with STORAGE_DRIVER=r2; each request gets its own database connection, which Hyperdrive keeps warm.

Secrets in a cloud deployment

The vault sidecar cannot run on Workers. Either stay on link mode, or keep bw-serve on your own network and reach it through a tunnel that only Cloudflare Access can traverse, with the Worker presenting a service token:

npx wrangler secret put BW_SERVE_URL                  # https://vault-bridge.yourdomain.com
npx wrangler secret put BW_SERVE_ACCESS_CLIENT_ID
npx wrangler secret put BW_SERVE_ACCESS_CLIENT_SECRET

If Access refuses the token, or the sidecar is down, secret fields degrade to link mode and say so.