Write a provider
Until the dedicated plugins ship, @valetkey/custom covers any auth system you can sign into with code. You write one function; valetkey handles storage, expiry, guardrails, and delivery.
The mint contract
Section titled “The mint contract”mint(ctx, persona) signs in server-side and returns the browser state that makes that sign-in real. Most apps need one fetch to their own sign-in endpoint and the resulting cookie:
import { createSessionArtifact, custom } from "@valetkey/custom";import { defineConfig } from "valetkey";
export default defineConfig({ app: { name: "myapp", origins: ["http://localhost:3000"], }, provider: custom({ mint: async (ctx, persona) => { const password = await ctx.secrets.get("DEV_USER_PASSWORD"); const response = await ctx.fetch("http://localhost:3000/api/sign-in", { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ email: persona.seed.email, password }), redirect: "manual", }); if (!response.ok && response.status !== 302) { throw new Error(`sign-in failed with ${response.status}`); } const setCookie = response.headers.getSetCookie()[0] ?? ""; const [pair = ""] = setCookie.split(";", 1); const separator = pair.indexOf("="); return createSessionArtifact({ origins: ctx.origins, cookies: [ { name: pair.slice(0, separator), value: pair.slice(separator + 1), domain: "localhost", path: "/", expires: -1, httpOnly: true, secure: false, sameSite: "Lax", }, ], meta: { email: String(persona.seed.email), role: String(persona.seed.role ?? "") }, }); }, }), personas: { admin: { seed: { email: "admin@myapp.test", role: "admin" } }, "free-user": { seed: { email: "free@myapp.test", role: "user" } }, },});Things valetkey does for you after mint() returns:
- Clamps
expiresAtto the session TTL. Returningnullis fine; valetkey fills in the deadline. - Rejects the artifact if any cookie’s domain is not one of your configured origins.
- Saves it with
0600permissions and writes the audit event.
Things to know while writing it:
ctx.secrets.get("KEY")is the only way to read the vault. Every value it returns is auto-registered for redaction, so it cannot leak through logs or errors.ctx.fetchis the standard fetch. Useredirect: "manual", because sign-in endpoints usually redirect and you want theSet-Cookiefrom the first response.- Some frameworks split the session across several cookies. Return them all;
response.headers.getSetCookie()gives you the full list. - Apps that keep the session in localStorage instead of cookies can return
localStorage: [{ origin, entries }]on the artifact.
Seeding users
Section titled “Seeding users”If the persona’s user might not exist yet, implement seed. It runs on valetkey seed and should be idempotent:
custom({ seed: async (ctx, persona) => { const adminKey = await ctx.secrets.get("ADMIN_API_KEY"); await ctx.fetch("http://localhost:3000/api/dev/users", { method: "PUT", headers: { authorization: `Bearer ${adminKey}` }, body: JSON.stringify(persona.seed), }); }, mint: async (ctx, persona) => { // ... },});Verification and revocation
Section titled “Verification and revocation”Both are optional. verify(ctx, artifact) returns "valid", "expired", or "invalid", useful when your app can invalidate sessions server-side. revoke(ctx, artifact) runs on valetkey logout; use it to kill the session inside your app, since valetkey already deletes the local artifact either way:
revoke: async (ctx, artifact) => { await ctx.fetch("http://localhost:3000/api/sessions/revoke", { method: "POST", body: JSON.stringify({ userId: artifact.meta.userId }), });},Skipping revoke is acceptable for dev; the artifact is deleted locally either way and the short TTL bounds the rest.
The exact types for all of this are in the Plugin API reference.