Blob: docs/migrations/bland.md
Migration: bland → tessera
Apply this in a separate PR against ~/code/bland after tessera is live and validated. Do not modify bland from the tessera repo.
What changes
Bland today stores users.password_hash (Argon2id PHC, same shape as tessera's) and mints its own jose-signed JWTs (15-min access + 7-day refresh in bland_refresh httpOnly cookie). After this migration:
- Sign-in becomes a redirect to tessera's
/api/auth/oauth2/authorize. - Bland's
users.idand every FK that references it stay completely untouched. - A new
tessera_sub TEXT UNIQUEcolumn onusersis the join key. OIDC callback finds the local user bytessera_sub; first-time sign-in binds by email. users.password_hashis dropped after every active user has signed in via tessera at least once.hashPassword/verifyPasswordremoved fromsrc/worker/lib/auth.ts.- Bland's jose JWT session stays. The OIDC callback validates the tessera ID token, then mints bland's own access+refresh JWTs exactly as today. This minimizes downstream-middleware churn —
requireAuth/optionalAuthinsrc/worker/middleware/auth.tsare unchanged.
Code changes
1. New env / secrets
wrangler.jsonc vars:
"TESSERA_OIDC_ISSUER": "https://auth.limic.dev",
"TESSERA_OIDC_CLIENT_ID": "<minted in tessera /admin/clients>"wrangler secret put TESSERA_OIDC_CLIENT_SECRET.
2. New OIDC callback route
Add src/worker/routes/oidc.ts with handleOidcStart + handleOidcCallback (same shape as the anvil migration; see anvil.md § 2 — bland's tweak is that the callback mints jose tokens via createAccessToken / createRefreshToken from src/worker/lib/auth.ts and sets the bland_refresh cookie via setRefreshCookie exactly as routes/auth.ts:30-71 does today).
Mount:
app.get("/auth/start", handleOidcStart);
app.get("/auth/callback", handleOidcCallback);3. Replace POST /auth/login
src/worker/routes/auth.ts:30-71 — delete the password-form handler. The bland-side /login UI becomes a "Sign in with tessera" button that redirects to /auth/start.
4. Drop password_hash
-- New migration:
ALTER TABLE users ADD COLUMN tessera_sub TEXT;
CREATE UNIQUE INDEX idx_users_tessera_sub ON users(tessera_sub);
-- Follow-up, after every active user has tessera_sub populated:
ALTER TABLE users DROP COLUMN password_hash;Remove hashPassword, verifyPassword, and the Argon2 imports from src/worker/lib/auth.ts.
5. What stays unchanged
users.idand every FK column referencing it (workspaces.owner_id,memberships.user_id,invites.invited_by/accepted_by,pages.created_by,pageShares.grantee_id/created_by,uploads.uploaded_by).- jose-issued bland JWT session —
createAccessToken,createRefreshToken,setRefreshCookie,clearRefreshCookie,verifyAccessToken,requireAuth,optionalAuth. - All workspace / page / share / upload paths.
- Turnstile gating on remaining public surfaces (the sign-in form gating goes away with the form itself).
Order of operations
Same as anvil:
- Land tessera, validate against the test client.
- Register
blandas an OAuth client in tessera. Save the secret. - Deploy bland with the new OIDC routes plus the existing password path still in place.
- Have every active user sign in via tessera once —
tessera_subpopulates. - Ship the follow-up PR that drops
password_hashand removes the password code path.
Forced password reset applies — tessera does not carry over the bland-side PHC hashes.