Summary
Replace memo’s shared-password gate with a new Clerk application (Hobby) using GitHub OAuth only, and make note cloud sync multi-user-safe by scoping every note to the signed-in Clerk user_id. Same-browser BroadcastChannel tab sync stays; cross-device sync remains Postgres + poll — but per user after this change.
Why This Matters
memo is moving toward personal/desktop clients (Tauri later) and shared hosting. A single MEMO_PASSWORD over a global notes table cannot support identity, ownership, or safe multi-device sync. Clerk Hobby supports GitHub social login (up to 3 social providers). A dedicated Clerk app keeps memo’s user pool separate from sume.com / sume.so.
Conversation Context
- Repo:
chasehuh/memo (live memo.chasehuh.com).
- Clerk CLI today is logged in as
chase@sume.com. Existing Clerk apps: sume.com, sume.so, abgcmo.com — no chasehuh/memo app yet.
- Product decision: create a new Clerk application (do not reuse sume Clerk apps).
- Auth strategy: GitHub login as the primary (and initially only) sign-in method on Hobby.
- Ops: create a GitHub OAuth App (under
chasehuh or the operator’s GitHub account) and wire Client ID/Secret into Clerk production; development may use Clerk shared GitHub credentials first.
- “Sync” means: (1) same-browser tab draft sync via
BroadcastChannel, (2) cross-device persistence via user-scoped notes APIs + existing 1.5s poll — not a new realtime bus in this issue.
- Retire
MEMO_PASSWORD / MEMO_SECRET / memo_session after cutover (hard cut, no long dual-gate).
- Existing DB rows have no
user_id — must backfill to the owner’s Clerk user id (chasehuh GitHub) or archive.
Current Behavior
Auth
| Piece |
Detail |
| Gate |
proxy.ts (Next 16; no middleware.ts) |
| Helpers |
lib/auth.ts |
| Login UI |
app/login/page.tsx |
| Login API |
POST /api/auth/login → sets memo_session |
| Logout API |
POST /api/auth/logout |
| Cookie |
memo_session = {issuedAt}.{HMAC(issuedAt, MEMO_SECRET)}, 30d, httpOnly |
| Password |
shared MEMO_PASSWORD (timing-safe compare) |
Unauthed pages → redirect /login; unauthed /api/* → 401. Note/upload routes do not re-check auth beyond proxy.
Notes + sync
-- lib/db.ts ensureSchema
CREATE TABLE IF NOT EXISTS notes (
id UUID PRIMARY KEY,
title TEXT NOT NULL DEFAULT '',
body TEXT NOT NULL DEFAULT '',
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
- No
user_id. Anyone with the password sees the same pool.
- CRUD:
lib/notes.ts + app/api/notes/* — unscoped.
- Tab sync:
lib/tab-sync.ts → BroadcastChannel("memo.sync") (same origin/browser only): draft / upsert / delete.
- Cross-device today: shared DB +
memo-app.tsx poll ~1500ms when visible.
Env today
DATABASE_URL, MEMO_PASSWORD, MEMO_SECRET, optional MEDIA_UPLOAD_*. No Clerk vars.
Desired Behavior
- Clerk app: New application named
memo under the chasehuh operator account (chase@sume.com or a chasehuh Clerk org if created). Dev + production instances.
- Sign-in: GitHub OAuth via Clerk. Visiting
/login (or Clerk sign-in) → GitHub → return to memo authenticated. No shared password form.
- Session: Clerk session cookies replace
memo_session. App chrome “Lock” → Clerk sign-out.
- Notes ownership: Every note has
user_id = Clerk user.id. List/create/update/delete only for that user. Other users’ note IDs return 404 (no existence leak).
- Sync:
- Same browser: keep
BroadcastChannel (optionally rename channel to include user id to avoid rare multi-account tab collisions).
- Cross-device: poll/upsert against user-scoped APIs so each GitHub user only syncs their notes.
- Upload:
/api/upload requires Clerk auth; prefer object keys under memo/{userId}/… when media is configured.
- Docs/env:
.env.example + README document Clerk + GitHub setup; remove MEMO_PASSWORD / MEMO_SECRET.
Source Of Truth
Internal repo/source
proxy.ts — current auth gate (replace with Clerk)
lib/auth.ts — HMAC session helpers to remove
lib/db.ts / lib/notes.ts / lib/types.ts — schema + CRUD
lib/tab-sync.ts — same-browser sync
components/memo-app.tsx — poll, logout/lock, editor shell
app/login/page.tsx, app/api/auth/* — password auth surface
app/api/notes/*, app/api/upload/route.ts — API surface to protect + scope
.env.example, README.md
External docs/source
Proposed API / Schema
DB migration
ALTER TABLE notes ADD COLUMN IF NOT EXISTS user_id TEXT;
-- Backfill existing rows to owner Clerk user id (from GitHub login once), then:
ALTER TABLE notes ALTER COLUMN user_id SET NOT NULL;
CREATE INDEX IF NOT EXISTS notes_user_updated_at_idx ON notes (user_id, updated_at DESC);
Notes JSON (unchanged shape + ownership enforced server-side)
{
"id": "uuid",
"title": "string",
"body": "string",
"created_at": "ISO-8601",
"updated_at": "ISO-8601"
}
user_id is not required in client payloads; server sets it from auth().userId.
Env
DATABASE_URL=...
NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY=pk_...
CLERK_SECRET_KEY=sk_...
NEXT_PUBLIC_CLERK_SIGN_IN_URL=/login
NEXT_PUBLIC_CLERK_SIGN_UP_URL=/login
# optional media
MEDIA_UPLOAD_URL=...
MEDIA_UPLOAD_SECRET=...
Validation Rules
- All note mutations require Clerk
userId.
GET/PATCH/DELETE /api/notes/:id must verify notes.user_id = userId or 404.
GET /api/notes returns only current user’s notes.
- Unauthenticated HTML → Clerk sign-in; API → 401.
- Backward compatibility: password login endpoints removed after cutover.
Implementation Notes
Ops (before/with code)
clerk apps create "memo" (or Dashboard) under chasehuh operator.
- Enable GitHub SSO connection (Hobby).
- Dev: optional shared Clerk GitHub credentials.
- Prod: create GitHub OAuth App → set Authorization Callback URL from Clerk → paste Client ID/Secret into Clerk; add production domain
memo.chasehuh.com.
- Copy publishable + secret keys into Vercel + local
.env.local.
Likely files to modify
package.json — add @clerk/nextjs (current major)
app/layout.tsx — <ClerkProvider>
proxy.ts — Clerk middleware/auth.protect pattern for Next 16 proxy
app/login/page.tsx — Clerk <SignIn /> (GitHub) instead of password form
- Remove or gut
app/api/auth/login|logout, lib/auth.ts
lib/db.ts — migration / ensureSchema for user_id
lib/notes.ts — all queries take userId
app/api/notes/route.ts, app/api/notes/[id]/route.ts — auth() + scope
app/api/upload/route.ts — require Clerk user; key prefix by user
components/memo-app.tsx / settings-panel.tsx — signOut; drop password lock
lib/tab-sync.ts — optional per-user channel name
.env.example, README.md
Flow
- User hits app → Clerk session check.
- No session →
/login → GitHub OAuth via Clerk → redirect home.
- Home loads
listNotes(userId) only.
- Edits save via PATCH with ownership check; other tabs get BroadcastChannel events; other devices pick up via poll.
- Sign out clears Clerk session; notes inaccessible until GitHub sign-in again.
Tests
- Manual: GitHub sign-in/out; create note as user A; confirm user B (second GitHub) cannot see it.
- Manual: two tabs same user — draft BroadcastChannel still works.
- Manual: two browsers/devices same GitHub — notes appear after poll/save.
pnpm build / tsc clean.
- Regression: CM editor, image upload (if env set), ⌘B sidebar, themes.
Edge Cases And Risks
- Existing notes backfill: Must assign
user_id before NOT NULL or app breaks. Prefer one-time SQL with owner’s Clerk id after first GitHub login.
- Clerk branding on Hobby sign-in UI (acceptable).
- Wrong Clerk app: Using sume instance would mix users — forbidden.
- Upload abuse: Auth alone is required; user-prefixed keys reduce collision.
- Proxy vs middleware: Project uses
proxy.ts — follow current Next/Clerk guidance for this repo; do not invent a second gate.
- Session cookie domain / production
memo.chasehuh.com must be allowlisted in Clerk.
Non-Goals
- Organizations / multi-seat / billing.
- Passkeys, MFA, email/password (GitHub-only for v1).
- Realtime websocket sync (Supabase realtime, PartyKit, etc.).
- Tauri / iOS shells (separate later).
- Migrating historical notes to multiple users (single owner backfill only).
- Removing Clerk branding (Pro).
Acceptance Criteria
QA Plan
- Create Clerk app + enable GitHub; set env locally.
pnpm install && pnpm dev — sign in with GitHub; create/edit/delete notes.
- Incognito as second GitHub account — empty list; cannot fetch first user’s note id.
- Two windows same user — type in one, confirm BroadcastChannel draft; save and confirm poll.
- Deploy Vercel env keys; production GitHub OAuth callback; smoke
memo.chasehuh.com.
- Run SQL backfill for legacy rows; verify owner still sees them.
Suggested PR Scope
Split recommended:
- PR A (Ops + Clerk shell): New Clerk app wiring,
ClerkProvider, replace /login + proxy gate, sign-out. Temporary: may still show all notes until PR B (call out clearly) or land behind flag.
- PR B (Data):
user_id migration + scoped lib/notes + API ownership + upload key prefix + backfill.
- Prefer A+B in one PR if cutover window is short (single hard cut) — acceptable for this small codebase.
Suggested next agent: $generate-pr / worktree-task from this issue; use Clerk CLI (clerk apps create, clerk enable) where possible; GitHub OAuth App creation may need human in github.com/settings/developers.
Summary
Replace memo’s shared-password gate with a new Clerk application (Hobby) using GitHub OAuth only, and make note cloud sync multi-user-safe by scoping every note to the signed-in Clerk
user_id. Same-browserBroadcastChanneltab sync stays; cross-device sync remains Postgres + poll — but per user after this change.Why This Matters
memo is moving toward personal/desktop clients (Tauri later) and shared hosting. A single
MEMO_PASSWORDover a globalnotestable cannot support identity, ownership, or safe multi-device sync. Clerk Hobby supports GitHub social login (up to 3 social providers). A dedicated Clerk app keeps memo’s user pool separate from sume.com / sume.so.Conversation Context
chasehuh/memo(livememo.chasehuh.com).chase@sume.com. Existing Clerk apps:sume.com,sume.so,abgcmo.com— no chasehuh/memo app yet.chasehuhor the operator’s GitHub account) and wire Client ID/Secret into Clerk production; development may use Clerk shared GitHub credentials first.BroadcastChannel, (2) cross-device persistence via user-scoped notes APIs + existing 1.5s poll — not a new realtime bus in this issue.MEMO_PASSWORD/MEMO_SECRET/memo_sessionafter cutover (hard cut, no long dual-gate).user_id— must backfill to the owner’s Clerk user id (chasehuh GitHub) or archive.Current Behavior
Auth
proxy.ts(Next 16; nomiddleware.ts)lib/auth.tsapp/login/page.tsxPOST /api/auth/login→ setsmemo_sessionPOST /api/auth/logoutmemo_session={issuedAt}.{HMAC(issuedAt, MEMO_SECRET)}, 30d, httpOnlyMEMO_PASSWORD(timing-safe compare)Unauthed pages → redirect
/login; unauthed/api/*→ 401. Note/upload routes do not re-check auth beyond proxy.Notes + sync
user_id. Anyone with the password sees the same pool.lib/notes.ts+app/api/notes/*— unscoped.lib/tab-sync.ts→BroadcastChannel("memo.sync")(same origin/browser only):draft/upsert/delete.memo-app.tsxpoll ~1500ms when visible.Env today
DATABASE_URL,MEMO_PASSWORD,MEMO_SECRET, optionalMEDIA_UPLOAD_*. No Clerk vars.Desired Behavior
memounder the chasehuh operator account (chase@sume.comor a chasehuh Clerk org if created). Dev + production instances./login(or Clerk sign-in) → GitHub → return to memo authenticated. No shared password form.memo_session. App chrome “Lock” → Clerk sign-out.user_id= Clerkuser.id. List/create/update/delete only for that user. Other users’ note IDs return 404 (no existence leak).BroadcastChannel(optionally rename channel to include user id to avoid rare multi-account tab collisions)./api/uploadrequires Clerk auth; prefer object keys undermemo/{userId}/…when media is configured..env.example+ README document Clerk + GitHub setup; removeMEMO_PASSWORD/MEMO_SECRET.Source Of Truth
Internal repo/source
proxy.ts— current auth gate (replace with Clerk)lib/auth.ts— HMAC session helpers to removelib/db.ts/lib/notes.ts/lib/types.ts— schema + CRUDlib/tab-sync.ts— same-browser synccomponents/memo-app.tsx— poll, logout/lock, editor shellapp/login/page.tsx,app/api/auth/*— password auth surfaceapp/api/notes/*,app/api/upload/route.ts— API surface to protect + scope.env.example,README.mdExternal docs/source
whoami→chase@sume.com; create new app (do not attach memo to sume.com / sume.so instances)Proposed API / Schema
DB migration
Notes JSON (unchanged shape + ownership enforced server-side)
{ "id": "uuid", "title": "string", "body": "string", "created_at": "ISO-8601", "updated_at": "ISO-8601" }user_idis not required in client payloads; server sets it fromauth().userId.Env
DATABASE_URL=... NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY=pk_... CLERK_SECRET_KEY=sk_... NEXT_PUBLIC_CLERK_SIGN_IN_URL=/login NEXT_PUBLIC_CLERK_SIGN_UP_URL=/login # optional media MEDIA_UPLOAD_URL=... MEDIA_UPLOAD_SECRET=...Validation Rules
userId.GET/PATCH/DELETE /api/notes/:idmust verifynotes.user_id = userIdor 404.GET /api/notesreturns only current user’s notes.Implementation Notes
Ops (before/with code)
clerk apps create "memo"(or Dashboard) under chasehuh operator.memo.chasehuh.com..env.local.Likely files to modify
package.json— add@clerk/nextjs(current major)app/layout.tsx—<ClerkProvider>proxy.ts— Clerk middleware/auth.protectpattern for Next 16 proxyapp/login/page.tsx— Clerk<SignIn />(GitHub) instead of password formapp/api/auth/login|logout,lib/auth.tslib/db.ts— migration / ensureSchema foruser_idlib/notes.ts— all queries takeuserIdapp/api/notes/route.ts,app/api/notes/[id]/route.ts—auth()+ scopeapp/api/upload/route.ts— require Clerk user; key prefix by usercomponents/memo-app.tsx/settings-panel.tsx— signOut; drop password locklib/tab-sync.ts— optional per-user channel name.env.example,README.mdFlow
/login→ GitHub OAuth via Clerk → redirect home.listNotes(userId)only.Tests
pnpm build/tscclean.Edge Cases And Risks
user_idbeforeNOT NULLor app breaks. Prefer one-time SQL with owner’s Clerk id after first GitHub login.proxy.ts— follow current Next/Clerk guidance for this repo; do not invent a second gate.memo.chasehuh.commust be allowlisted in Clerk.Non-Goals
Acceptance Criteria
memoexists (dev + path to production) under chasehuh operator — not sume apps.MEMO_PASSWORD/MEMO_SECRET/memo_sessionremoved from runtime path and docs.notes.user_idexists, indexed, NOT NULL; all CRUD scoped.QA Plan
pnpm install && pnpm dev— sign in with GitHub; create/edit/delete notes.memo.chasehuh.com.Suggested PR Scope
Split recommended:
ClerkProvider, replace/login+ proxy gate, sign-out. Temporary: may still show all notes until PR B (call out clearly) or land behind flag.user_idmigration + scopedlib/notes+ API ownership + upload key prefix + backfill.Suggested next agent:
$generate-pr/ worktree-task from this issue; use Clerk CLI (clerk apps create,clerk enable) where possible; GitHub OAuth App creation may need human in github.com/settings/developers.