Skip to content

Clerk GitHub auth + per-user note sync (new memo Clerk app) #17

Description

@chasehuh

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.comno 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.tsBroadcastChannel("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

  1. Clerk app: New application named memo under the chasehuh operator account (chase@sume.com or a chasehuh Clerk org if created). Dev + production instances.
  2. Sign-in: GitHub OAuth via Clerk. Visiting /login (or Clerk sign-in) → GitHub → return to memo authenticated. No shared password form.
  3. Session: Clerk session cookies replace memo_session. App chrome “Lock” → Clerk sign-out.
  4. 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).
  5. 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.
  6. Upload: /api/upload requires Clerk auth; prefer object keys under memo/{userId}/… when media is configured.
  7. 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)

  1. clerk apps create "memo" (or Dashboard) under chasehuh operator.
  2. Enable GitHub SSO connection (Hobby).
  3. Dev: optional shared Clerk GitHub credentials.
  4. Prod: create GitHub OAuth App → set Authorization Callback URL from Clerk → paste Client ID/Secret into Clerk; add production domain memo.chasehuh.com.
  5. 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.tsauth() + 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

  1. User hits app → Clerk session check.
  2. No session → /login → GitHub OAuth via Clerk → redirect home.
  3. Home loads listNotes(userId) only.
  4. Edits save via PATCH with ownership check; other tabs get BroadcastChannel events; other devices pick up via poll.
  5. 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

  • New Clerk application memo exists (dev + path to production) under chasehuh operator — not sume apps.
  • GitHub OAuth works for sign-up/sign-in on local and production domain.
  • MEMO_PASSWORD / MEMO_SECRET / memo_session removed from runtime path and docs.
  • notes.user_id exists, indexed, NOT NULL; all CRUD scoped.
  • User A cannot read/write User B’s notes (404).
  • Same-browser tab draft sync still works for one user.
  • Cross-device: save on device 1 appears on device 2 after poll for same GitHub user.
  • Settings/Lock signs out via Clerk.
  • README documents Clerk + GitHub OAuth App setup.

QA Plan

  1. Create Clerk app + enable GitHub; set env locally.
  2. pnpm install && pnpm dev — sign in with GitHub; create/edit/delete notes.
  3. Incognito as second GitHub account — empty list; cannot fetch first user’s note id.
  4. Two windows same user — type in one, confirm BroadcastChannel draft; save and confirm poll.
  5. Deploy Vercel env keys; production GitHub OAuth callback; smoke memo.chasehuh.com.
  6. Run SQL backfill for legacy rows; verify owner still sees them.

Suggested PR Scope

Split recommended:

  1. 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.
  2. PR B (Data): user_id migration + scoped lib/notes + API ownership + upload key prefix + backfill.
  3. 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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions