The write-only credential vault
The problem: your app needs a real credential — a Bluesky app password, a Meta API token, a webhook signing key that must rotate. Where does it live? Not in the source (public, remixable). Not in a normal document (it would sync to every reader's browser). Traditionally this is where you provision a secret manager.
The pattern: a document on a channel granted to no one.
js// access.js
export default function (doc, oldDoc, user, ctx) {
if (!user || !user.isOwner) throw { forbidden: "owner-only" };
if (doc.kind === "token") {
// Write-only intake: the owner can WRITE token docs (the paste form,
// the scheduled rotator), but the `vault` channel is granted to NO ONE,
// so raw token values never sync to any browser — not even the owner's.
return { channels: ["vault"] };
}
if (doc.kind === "token-status" || doc.kind === "oplog") {
return { channels: ["ops"], grant: { roles: { owner: ["ops"] } } };
}
throw { forbidden: "unknown document type" };
}
Reads are routed by channel grants, and the vault channel has none — so
there is no client read path that returns a token value. The only thing
that can read the vault is the backend's scheduled handler: it runs as the
owner in admin mode, where ctx.db.query({ db: "vault" }) is unfiltered.
fetch and onChange can't — access-fn-bound databases aren't queryable
from those lanes, which makes the vault structurally unreadable from
anything a visitor can trigger.
js// backend.js — the only reader
export const config = { scheduled: { interval: "1m" } };
export async function scheduled(event, ctx) {
const vault = await ctx.db.query({ db: "vault" });
const cred = vault.find((d) => d._id === "token-bsky");
if (!cred) return;
// ...use cred.token; never write its value anywhere readable.
await ctx.db.put(
{ _id: "token-status-bsky", kind: "token-status", ok: true, last4: cred.token.slice(-4), checkedAt: event.scheduledTime },
{ db: "oplog" }
);
}
The dashboard shows a redacted projection (token-status: verified
account name, expiry countdown, NEEDS RE-AUTH) — never the value. Pasting a
fresh credential is just an ordinary document write from the owner's browser:
write-in is allowed, read-back isn't.
Why not ctx.secrets?
ctx.secrets is the right home for a small,
static key the owner sets once. The vault earns its keep when credentials are
data: they rotate on a schedule (the backend writes refreshed tokens back
to the vault), carry metadata (expiry, account id, verification state), come
in multiples per platform, and drive UI state. Secrets are config; the vault
is a credential lifecycle.
Trust boundary, stated plainly
Tokens are protected from every other user and from every browser, and they live server-side. The app's owner is trusting the Vibes DIY platform the way they'd trust any SaaS that holds API keys — no more, no less.
Remix a running one
meta-hub — the console that publishes this site's blog to five social platforms — is this pattern in production: vault + redacted status + scheduled rotation. Remix it and the whole security model transfers to you: remix copies code, never data, so your vault starts empty and the original tokens stay home. The full operating manual is its RUNBOOK, and the story is Remix the machine that posts.