Implementation Plan

Document Type: Living Implementation Plan
Context: Solo developer, hobby-to-business trajectory, AI-assisted with full code review
Based on: Production-Grade Architecture Handover (May 2026)

Tasks are grouped into phases. Each phase is a shippable milestone — by the end of it, something real and working exists that you can use, demo, or build on top of. Phases are sequenced to minimize re-work and to teach progressively harder concepts.

Complexity tags:
🟢 Beginner-friendly — follow-the-docs territory
🟡 Intermediate — requires understanding the “why”
🔴 Advanced — architectural weight-bearing, get it right first time

Learning tags:
📦 Monorepo / toolchain
🔐 Auth / Security
🗄️ Database / Drizzle
🧩 Next.js patterns
🎨 UI / Mantine
✍️ Editor / Tiptap
🤖 AI / Embeddings
📱 Mobile / PWA
🖥️ CLI / API
💳 Billing / SaaS

Milestone: A working monorepo that builds, lints, and runs locally. The skeleton that every future phase lives inside.
Learning payoff: Turborepo, pnpm workspaces, TypeScript strict mode, project conventions.
Completed: May 2026

#TaskComplexityLearningStatus
0.1Initialize pnpm monorepo with pnpm init and workspace config🟢📦
0.2Set up Turborepo with turbo.json — define build, dev, lint, typecheck pipelines🟢📦
0.3Create apps/web as a Next.js 16 App Router project with TypeScript strict🟢🧩 📦
0.4Create apps/cli as a bare TypeScript package (empty for now, will be wired up later)🟢📦
0.5Create packages/core — empty package with correct package.json and tsconfig🟢📦
0.6Create packages/ui — empty package, will house shared Mantine components🟢📦
0.7Create packages/features-registry — empty, will house ALL_FEATURES manifest🟢📦
0.8Configure shared tsconfig.base.json at root; extend it in all packages🟡📦
0.9Add ESLint + Prettier with boundary enforcement; wire into Turborepo lint pipeline🟡📦
0.10Add .env.example template and document all required environment variables (see §20.1)🟢
0.11Set up Vercel project, link repo, configure environment variables in dashboard🟢
0.12Confirm turbo dev starts apps/web correctly from the monorepo root🟢📦
0.13Add a root README.md documenting how to run, build, and add packages🟢

Phase 0 Exit Criteria: ✅ All met.

  • pnpm turbo build completes without errors or warnings
  • pnpm turbo dev starts apps/web at localhost:3000
  • pnpm turbo lint and pnpm turbo typecheck pass across all 5 packages
  • Dependency boundary enforcement active — packages/*apps/* blocked by ESLint
  • Deployed to Vercel at https://sidekick-six-bay.vercel.app

Implementation notes:

  • Next.js 16 was current at time of implementation (plan said 15 — updated in place)
  • pnpm 11.0.8 via Corepack (plan said pnpm@8 — Corepack handles version enforcement)
  • apps/web/eslint.config.mjs generated by scaffolder was removed — root eslint.config.js handles all packages
  • apps/web/pnpm-lock.yaml generated by scaffolder was removed — root lockfile manages the workspace
  • packages/features-registry not packages/feature-registry — corrected after initial scaffold
  • turbo.json build.env array added to prevent stale Vercel cache when env vars change
  • Repo made public intentionally — see architecture handover §21

Milestone: A real login screen that works. Protected routes. A profile in the database. The security skeleton everything else hangs on.
Learning payoff: Supabase auth, cookie sessions, Next.js middleware, server vs browser client separation.
Completed: May 2026

#TaskComplexityLearningStatus
1.1Create Supabase project; enable email/password auth🟢🔐
1.2Install Supabase client packages in packages/core🟢🔐
1.3Implement createBrowserClient() helper in packages/core/supabase/browser.ts🟡🔐
1.4Implement createServerClient() helper in packages/core/supabase/server.ts🟡🔐
1.5Implement admin/service-role client in packages/core/supabase/admin.ts — server only🔴🔐
1.6Write profiles table schema in packages/core using Drizzle; add id, email, createdAt🟡🗄️
1.7Set up drizzle.config.ts in packages/core; configure migrations folder🟡🗄️
1.8Add a root pnpm db:migrate script that discovers and runs all package migrations in order🔴🗄️ 📦
1.9Enable RLS on profiles; add the canonical user-owns-rows policy🔴🔐 🗄️
1.10Implement withRLS(userId, fn) helper in packages/core/db/rls.ts🔴🔐 🗄️
1.11Implement Next.js proxy in apps/web — session refresh, redirect unauthenticated users, exclude /api/*🔴🧩 🔐
1.12Build login page UI with Mantine form components🟢🎨
1.13Build sign-up page UI with email/password🟢🎨
1.14Add Mantine provider, Notifications, and PostCSS config (see §20.4)🟡🎨
1.15Implement post-login redirect to /dashboard🟢🧩
1.16Build a minimal dashboard shell layout (sidebar navigation placeholder, header)🟢🧩 🎨
1.17Implement sign-out functionality🟢🔐
1.18Verify session persists across page reloads; verify redirect works for unauthenticated users🟢

Phase 1 Exit Criteria: ✅ All met.

  • You can sign up, log in, and see a dashboard
  • Unauthenticated access to /dashboard redirects to login
  • RLS is enabled on profiles

Implementation notes:

  • Next.js 16 renamed middleware → proxy. File: middleware.tsproxy.ts. Export: export function middlewareexport function proxy. The config export is unchanged. This affects how session refresh and route protection work at the edge.
  • Profile creation via Postgres trigger, not API route. Trigger on_auth_user_created on auth.users calls create_profile_for_new_user(). The function must reference public.profiles (fully qualified) because triggers run in the auth schema context. This approach handles all auth providers (email, OAuth, magic link) without per-provider app-level code, and cannot fail silently after auth succeeds.
  • CSS modules only. No inline styles. No Mantine style props. packages/eslint-plugin-sidekick with a no-mantine-style-props rule enforces this. Mantine behavioral props (withBorder, shadow, navbar={{ width, breakpoint }}) are allowed; pure style props (h, px, fw, c, mt, size, color, justify, gap) are banned.
  • 4 Supabase clients. A fourth client, createProxyClient(request, response), was added for the Edge runtime. It reads cookies from the incoming request/response directly and never imports next/headers (which is Node.js-only). Used exclusively in proxy.ts.
  • packages/copy added. Centralized string copy shared across all apps. Never hardcode user-visible strings in source files — import from packages/copy instead.
  • useNavigation hook added. Always calls router.push() + router.refresh() together. Prevents forgetting the refresh step after auth actions that change server-rendered state.
  • export const dynamic = 'force-dynamic' required. Must be set on all route groups that touch Supabase (e.g. (app)/layout.tsx, (auth)/layout.tsx). Prevents Next.js from pre-rendering server-rendered auth-dependent routes at build time.
  • dotenv-cli pattern for env loading. .env.local lives at the repo root only. All scripts that need env vars prefix with dotenv -e ../../.env.local --. Node.js --env-file flag is blocked as a security measure by Node.js itself.
  • GraphQL + Relay deferred to post-MVP. REST is sufficient for MVP. Relay + App Router friction is unresolved upstream. withApiGuard maps cleanly to REST. Can add GraphQL later without a full rewrite.
  • API versioning (/api/v1/) deferred to post-MVP. Current routes are at /api/. Versioning adds URL complexity for no current benefit — MVP has one client and breaking changes can be coordinated directly.
  • DATABASE_DIRECT_URL (port 5432) for migrations only; DATABASE_URL (port 6543) for runtime. DATABASE_DIRECT_URL is NOT needed in Vercel — migrations run locally, never on Vercel.
  • Supabase renamed keys in 2025. NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY replaces the old NEXT_PUBLIC_SUPABASE_ANON_KEY. SUPABASE_SECRET_KEY replaces the old SUPABASE_SERVICE_ROLE_KEY.

Milestone: Drizzle connects as a non-superuser role (app_runtime). RLS is enforced at the database level — not by convention. Soft-delete filtering, user-data isolation, and hard-delete prevention are guaranteed by the database regardless of what application code does. Learning payoff: PostgreSQL roles, RLS enforcement vs. convention, trigger functions, migration journal mechanics, connection pooling and session variable scoping. Completed: May 2026

#TaskComplexityLearningStatus
1.1.1Create app_runtime PostgreSQL role via Supabase SQL Editor (not in git — contains password)🟢🗄️ 🔐
1.1.2Create migration 0001_app_runtime_grants.sql — GRANT + ALTER DEFAULT PRIVILEGES🟡🗄️ 🔐
1.1.3Create migration 0002_profiles_rls_policy.sql — formalize profiles RLS policy in version control🟡🗄️ 🔐
1.1.4Create migration 0003_soft_delete_trigger_fns.sql — shared enforce_soft_delete() and block_update_on_deleted() functions🟡🗄️
1.1.5Register all three migrations in meta/_journal.json with increasing when timestamps🟢🗄️
1.1.6Run pnpm db:migrate — apply all three migrations🟢🗄️
1.1.7Drop the dashboard-created profiles policy that predated version-controlled migrations🟢🗄️
1.1.8Update DATABASE_URL in .env.local to use app_runtime credentials (pooler, port 6543)🟢🔐
1.1.9Fix withRLS — wrap in db.transaction() so set_config is properly transaction-scoped🔴🗄️ 🔐
1.1.10Verify: pg_roles, role_table_grants, pg_policies queries confirm setup; app loads after DATABASE_URL change🟢

Phase 1.1 Exit Criteria: ✅ All met.

  • app_runtime role exists with SELECT/INSERT/UPDATE/DELETE on all public tables
  • DEFAULT PRIVILEGES ensures future tables are auto-granted
  • profiles RLS policy is in version control and enforced
  • withRLS wraps queries in a real transaction — no session variable leakage
  • Trigger functions enforce_soft_delete() and block_update_on_deleted() exist, ready to bind in Phase 3+

Implementation notes:

  • SET ROLE in Supabase SQL Editor is restricted — cannot be used to test role-based access. Verify via pg_roles, information_schema.role_table_grants, and pg_policies. True end-to-end test is the running application.
  • Migration when timestamps must be strictly increasing. Always base new timestamps on the previous entry.
  • db:generate is NOT run for hand-written SQL migrations. Custom SQL goes straight to db:migrate.
  • Two policies existed on profiles after migration — the original dashboard policy had a different name and was not dropped by the migration. Required a manual DROP POLICY afterward.
  • Trigger functions defined now (not deferred to Phase 3) so feature migrations only need CREATE TRIGGER bindings.

Milestone: The architectural backbone is live. withApiGuard is implemented and tested. The feature registry exists. You could add any feature safely from here.
Learning payoff: Middleware patterns, centralized auth, feature flags, the “why” behind the architecture.

#TaskComplexityLearning
2.1Implement resolveApiCaller(req) in packages/core/api/auth.ts — handles both cookie sessions and Bearer API keys🔴🔐 🧩
2.2Implement withApiGuard(handler, opts) in packages/core/api/guard.ts (see §10.2 canonical implementation)🔴🔐 🧩
2.3Wire withRLS inside withApiGuard🔴🔐 🗄️
2.4Add basic request logging inside withApiGuard (method, path, userId, latency)🟡🧩
2.5Add auth failure logging inside withApiGuard🟡🧩
2.6Define FeatureManifest type in packages/features-registry🟡📦
2.7Implement ALL_FEATURES array in packages/features-registry/index.ts — start with an empty array🟡📦
2.8Create user_feature_entitlements table in packages/core with userId, featureSlug, RLS policy🔴🗄️ 🔐
2.9Implement getEnabledFeatures(userId) in packages/core — reads from entitlements table🟡🗄️
2.10Write a seed script to enable all features for your own user account during development🟢🗄️
2.11Create a test API route /api/health using withApiGuard to verify the full guard chain works🟡🧩 🔐
2.12Verify 401 is returned when unauthenticated; 403 when a feature is disabled🟢

Phase 2 Exit Criteria: withApiGuard is implemented and the /api/health route correctly returns 401/403 in the right conditions. The feature system can enable/disable features per user.

Milestone: A fully working Notes feature — create, read, update, soft-delete. This is your “proof of architecture” feature. Every future feature follows this exact same pattern.
Learning payoff: The complete feature loop: schema → migration → API → repository → UI. This is the template you’ll reuse for every other feature.

#TaskComplexityLearning
3.1Create packages/feature-notes with its own package.json, tsconfig, and Drizzle config🟡📦 🗄️
3.2Define notes table schema in packages/feature-notes/schema.ts — include id (client UUID), userId, title, content, createdAt, updatedAt, deletedAt, embeddingStatus🔴🗄️
3.3Run migration; verify table appears in Supabase dashboard🟢🗄️
3.4Enable RLS on notes; add canonical user-owns-rows policy🔴🔐 🗄️
3.5Register notes feature in packages/features-registry/ALL_FEATURES🟢📦
3.6Implement NotesRepository in packages/feature-notes/repository.tslist(), getById(), create(), update(), softDelete()🔴🗄️
3.7Implement GET /api/notes route using withApiGuard + feature guard + notes:read scope🔴🧩 🔐
3.8Implement POST /api/notes route — accept client-generated UUID🔴🧩 🔐
3.9Implement GET /api/notes/[id] route🟡🧩
3.10Implement PATCH /api/notes/[id] route🟡🧩
3.11Implement DELETE /api/notes/[id] route — soft delete only, set deletedAt🟡🧩 🗄️
3.12Build Notes list page at /notes — server component fetching notes via repository🟡🧩 🎨
3.13Build Note detail/edit page at /notes/[id]🟡🧩 🎨
3.14Build “New Note” flow with client-generated UUID🟡🧩 🎨
3.15Wire up soft-delete in the UI with confirmation🟢🎨
3.16Enable the notes feature for your own user account via seed script🟢
3.17Manually test the full CRUD loop via both UI and direct API calls (use curl or Postman)🟢

Phase 3 Exit Criteria: Notes can be created, edited, listed, and soft-deleted through the UI. The API layer enforces auth and feature entitlement. All queries filter where(isNull(notes.deletedAt)).

Note on task 3.4: The RLS policy for notes uses the combined pattern (user isolation + soft-delete in one USING clause) established in Phase 1.1. The migration must also bind the two shared trigger functions created in 0003. See the Phase 3+ template in the architecture handover.

-- RLS (combined pattern)
ALTER TABLE notes ENABLE ROW LEVEL SECURITY;
ALTER TABLE notes FORCE ROW LEVEL SECURITY;
CREATE POLICY "users_own_rows" ON notes FOR ALL
  USING (user_id::text = current_setting('app.current_user_id', true) AND deleted_at IS NULL)
  WITH CHECK (user_id::text = current_setting('app.current_user_id', true));

-- Trigger bindings (functions already exist from migration 0003)
CREATE TRIGGER no_hard_delete_notes
  BEFORE DELETE ON notes FOR EACH ROW EXECUTE FUNCTION enforce_soft_delete();
CREATE TRIGGER no_update_deleted_notes
  BEFORE UPDATE ON notes FOR EACH ROW EXECUTE FUNCTION block_update_on_deleted();

Milestone: A proper writing experience with Tiptap. Notes and Writing share the editor component. This is where the app starts feeling real.
Learning payoff: Tiptap configuration, rich text as JSON storage, markdown export, editor extensions.

#TaskComplexityLearning
4.1Install Tiptap dependencies in packages/ui🟢✍️
4.2Build a <RichTextEditor> component in packages/ui using @mantine/tiptap🟡✍️ 🎨
4.3Configure extensions: Bold, Italic, Heading, BulletList, OrderedList, Code, Link, Image🟡✍️
4.4Implement JSON storage — editor outputs editor.getJSON() for storage🔴✍️
4.5Implement markdown export — editor.storage.markdown.getMarkdown() for embedding pipeline🔴✍️
4.6Make editor mobile-friendly (touch targets, mobile toolbar)🟡✍️ 📱
4.7Swap plain textarea in Notes editor for <RichTextEditor>🟢
4.8Create packages/feature-writing with its own schema🟡📦 🗄️
4.9Define documents table — similar shape to notes but with type (essay, journal, draft)🟡🗄️
4.10Implement full API routes for Writing feature (same pattern as Notes)🟡🧩
4.11Register writing feature in feature registry🟢
4.12Build Writing section in the app shell (/writing)🟡🧩 🎨

Phase 4 Exit Criteria: The rich text editor is shared, reusable, stores JSON, exports markdown. Notes and Writing both use it.

Milestone: The app is a multi-feature productivity tool. Three more features built using the established pattern.
Learning payoff: Repetition builds fluency. Schema design for varied content types. Simpler after Notes.

Each sub-feature follows the same pattern: schema → migration → RLS → feature registry → API routes → repository → UI pages.

5A — Bookmarks

#TaskComplexity
5A.1Create packages/feature-bookmarks; define bookmarks schema (url, title, description, tags, favicon)🟡
5A.2Migration, RLS, feature registry entry🟡
5A.3API routes: list, create, update, soft-delete🟡
5A.4Implement BookmarksRepository🟡
5A.5UI: Bookmarks list with search/filter by tag🟡
5A.6UI: Add bookmark form (URL, auto-fetch title/description via server action)🟡

5B — Recipes

#TaskComplexity
5B.1Create packages/feature-recipes; define recipes schema (title, ingredients, steps, tags, servings)🟡
5B.2Migration, RLS, feature registry entry🟡
5B.3API routes: list, create, update, soft-delete🟡
5B.4Implement RecipesRepository🟡
5B.5UI: Recipe list with search🟡
5B.6UI: Recipe detail with structured ingredients and steps🟡

5C — Budget

#TaskComplexity
5C.1Create packages/feature-budget; define transactions schema (amount, category, date, notes)🟡
5C.2Migration, RLS, feature registry entry🟡
5C.3API routes: list (with date range filter), create, update, soft-delete🟡
5C.4Implement BudgetRepository🟡
5C.5UI: Transaction list with monthly grouping🟡
5C.6UI: Simple spending summary by category🟡

Phase 5 Exit Criteria: All three content features are live, follow the same architectural patterns, and are protected by the feature entitlement system.

Milestone: Your content is semantically searchable. An AI chat interface can answer questions about your notes, bookmarks, and recipes. This is where the app becomes genuinely powerful.
Learning payoff: pgvector, HNSW indexes, semantic chunking, the Vercel AI SDK, streaming responses, RAG pipeline design.

#TaskComplexityLearning
6.1Enable pgvector extension in Supabase🟢🤖
6.2Add embedding vector column to notes, documents, bookmarks, recipes tables🟡🤖 🗄️
6.3Create HNSW indexes on each embedding column🔴🤖 🗄️
6.4Implement generateEmbedding(text) in packages/core/ai/embed.ts using OpenAI text-embedding-3-small🟡🤖
6.5Implement semantic chunking strategy — split tiptap JSON → markdown → chunks with overlap🔴🤖 ✍️
6.6Implement async background embedding job with retry (2 retries, exponential backoff) using waitUntil()🔴🤖 🧩
6.7Wire embedding generation into Notes create/update API routes — non-blocking, sets embeddingStatus🔴🤖 🔐
6.8Implement match_content() PostgreSQL function for vector similarity search🔴🤖 🗄️
6.9Create packages/feature-ai-chat with its own schema (chat_sessions, chat_messages)🟡📦 🗄️
6.10Migration, RLS, feature registry entry for AI Chat🟡
6.11Implement POST /api/ai/chat streaming route using Vercel AI SDK + Anthropic Claude🔴🤖 🧩
6.12Implement RAG context retrieval in the chat handler — retrieve top-k relevant chunks before calling LLM🔴🤖
6.13Build AI Chat UI in packages/feature-ai-chat — streaming message display, input, loading states🟡🎨 🧩
6.14Add /chat page to app shell🟢
6.15Implement embeddingStatus monitoring — a simple admin view showing failed embeddings🟡🤖
6.16Add retry trigger API endpoint for failed embeddings🟡🤖

Phase 6 Exit Criteria: You can type a question in the chat, it retrieves relevant chunks from your notes/recipes/bookmarks, and streams a contextual answer from Claude. Failed embeddings are visible and retriable.

Milestone: The app is installable on your iPhone. It works as a PWA in the browser and as a side-loaded Capacitor app.
Learning payoff: Service workers, PWA manifest, Capacitor, mobile UX constraints.

#TaskComplexityLearning
7.1Install and configure Serwist for service worker support in apps/web🟡📱
7.2Create manifest.json — app name, icons, display mode, theme color🟢📱
7.3Configure offline asset caching strategy in the service worker🟡📱
7.4Test PWA install flow in Chrome DevTools and on iPhone via Safari🟢📱
7.5Audit mobile UI — touch targets, viewport, safe areas, keyboard behavior🟡📱 🎨
7.6Initialize Capacitor in apps/web🟡📱
7.7Configure Capacitor to point at the hosted Vercel Next.js URL🟡📱
7.8Build and side-load the iOS app onto your iPhone using Xcode🟡📱
7.9Test core flows (login, notes, chat) on device🟢
7.10Handle iOS safe area insets in the layout🟡📱

Phase 7 Exit Criteria: App is installable as a PWA from Safari, side-loadable as an iOS app via Capacitor. Core features work on-device.

Milestone: You can use your own app from the terminal. API keys are manageable from the UI. The CLI is a real working tool.
Learning payoff: CLI tooling (commander.js or similar), API key security patterns, Bearer auth flows, streaming in a terminal context.

#TaskComplexityLearning
8.1Create api_keys table in packages/core (see §8.3 canonical schema)🟡🗄️ 🔐
8.2Migration + RLS for api_keys🟡🗄️ 🔐
8.3Implement API key generation — secure random bytes → raw key returned once → SHA-256 hash stored🔴🔐
8.4Wire Bearer API key lookup into resolveApiCaller() in packages/core🔴🔐
8.5Implement POST /api/api-keys — create key, return raw key once🔴🔐 🧩
8.6Implement GET /api/api-keys — list keys (no raw key values, show label/scopes/last-used)🟡🧩
8.7Implement DELETE /api/api-keys/[id] — revoke key by setting revokedAt🟡🧩
8.8Update withApiGuard to track lastUsedAt on successful API key auth🟡🔐
8.9Build API key management UI — list keys, create key (show raw key once), revoke🟡🎨
8.10Build apps/cli as a Node.js CLI tool — authenticate with API key from env/config file🟡🖥️
8.11Implement cli notes list command🟡🖥️
8.12Implement cli notes create command (from stdin or file)🟡🖥️
8.13Implement cli chat command with streaming output to terminal🔴🖥️ 🤖
8.14Document CLI usage in README🟢

Phase 8 Exit Criteria: You can generate an API key in the UI, set it as an env var, and run cli notes list and cli chat from your terminal.

Milestone: The app is reliable enough for friends and family. You have visibility into what’s failing. Error handling is consistent.
Learning payoff: Structured logging, error boundaries, rate limiting, the operational side of running a web service.

#TaskComplexityLearning
9.1Add structured request logging to withApiGuard — method, path, userId, latency, status🟡🧩
9.2Add auth failure logging — track 401/403 patterns🟡🔐
9.3Add failed embedding logging with enough context to retry🟡🤖
9.4Add Next.js error boundaries to all main UI sections🟡🧩
9.5Add global API error response normalization — consistent { error, code } shape🟡🧩
9.6Review and audit all API routes — confirm every route uses withApiGuard🟢🔐
9.7Audit all queries — confirm where(isNull(table.deletedAt)) on syncable tables🟢🗄️
9.8Add basic rate limiting on auth routes and AI chat endpoint🟡🔐
9.9Run a manual penetration test of your own app — try to access another user’s data🔴🔐
9.10Add input validation (zod) on all API route handlers🟡🧩

Phase 9 Exit Criteria: Logs are structured and useful. All routes are guarded. No RLS gaps. Input validation is consistent across the API.

Milestone: You can invite others to use the app. They have their own isolated data. You have a basic way to manage who has access.
Learning payoff: Multi-user ops, invite flows, feature management for different users.

#TaskComplexityLearning
10.1Build an invite flow — generate invite link that pre-approves sign-up🔴🔐 🧩
10.2Build a simple admin page (your user only) to list users and manage feature entitlements🟡🎨 🔐
10.3Add isAdmin flag to profiles table; gate admin pages behind it🟡🔐
10.4Enable specific features for invited users from the admin panel🟡
10.5Test full sign-up and feature access flow from a fresh incognito session🟢
10.6Collect feedback from dogfood users; create a prioritized bug list🟢

Phase 10 Exit Criteria: You can invite someone, they can sign up, they have access only to the features you enabled for them, and their data is fully isolated from yours.

Milestone: The app can charge for access. Feature entitlement is tied to subscription tier. The foundation for a real business offering.
Learning payoff: Stripe integration, webhook handling, subscription state management, SaaS architecture patterns.

#TaskComplexityLearning
11.1Create Stripe account; configure products and price tiers🟢💳
11.2Add subscriptions table — userId, stripeCustomerId, stripePriceId, status, currentPeriodEnd🟡🗄️ 💳
11.3Implement Stripe checkout session creation in POST /api/billing/checkout🟡💳 🧩
11.4Implement Stripe webhook handler — sync subscription state into subscriptions table🔴💳 🔐
11.5Update getEnabledFeatures() to resolve features from subscription tier as well as manual entitlements🔴💳 🗄️
11.6Build billing settings page — current plan, upgrade CTA, portal link🟡🎨 💳
11.7Implement Stripe customer portal redirect for managing subscriptions🟡💳
11.8Add a landing page or marketing page explaining features per tier🟢🎨
11.9End-to-end test: sign up → subscribe → gain feature access → cancel → lose access🟡

Phase 11 Exit Criteria: A new user who subscribes to a paid plan automatically gains access to paid features. A cancelled user loses access at period end.

Milestone: The app can execute automations on your behalf. External agents can interact with your data via the public API.
Learning payoff: Agent interoperability, webhook-driven automation, workflow design patterns.

#TaskComplexityLearning
12.1Design a workflows schema — trigger type, actions, enabled flag🔴🗄️
12.2Implement a simple recurring workflow — e.g., weekly digest of bookmarks emailed to self🟡🧩
12.3Add webhook ingest endpoint — external services can push events to the app🔴🔐 🧩
12.4Document the public API surface for agents — OpenAPI spec or equivalent🟡🖥️
12.5Evaluate Inngest for durable workflows if complexity warrants it🟡
12.6Add agent-friendly scoped API keys for automation use cases🟡🔐

If you’re new to some of the tech in this stack, here’s the minimum viable reading before each phase:

Before PhaseRead / Watch
Phase 0Turborepo docs getting started; pnpm workspaces
Phase 1Supabase Auth docs; Next.js App Router docs (routing, middleware, server components)
Phase 2Next.js Route Handlers; how middleware chains work
Phase 3Drizzle ORM quickstart; PostgreSQL RLS basics
Phase 4Tiptap getting started; @mantine/tiptap docs
Phase 6pgvector README; Vercel AI SDK docs; Anthropic API docs
Phase 7Serwist docs; Capacitor iOS quickstart
Phase 8Node.js CLI patterns (commander.js); API key security best practices
Phase 11Stripe docs: Checkout, webhooks, customer portal

Before moving to the next phase, verify:

  • Every new API route uses withApiGuard()
  • No packages/* imports from apps/*
  • Every new user-owned table has RLS enabled with the canonical combined policy (user isolation + soft-delete in one USING clause)
  • Every new syncable table has no_hard_delete_[table] and no_update_deleted_[table] triggers applied
  • Every new table with user content uses withRLS() via the guard, not inline
  • All mutations go through the repository layer
  • Syncable tables include createdAt, updatedAt, deletedAt
  • Soft deletes only — no hard deletes on user data
  • All queries on syncable tables filter where(isNull(table.deletedAt))
  • Content tables in the embedding pipeline have embeddingStatus
  • DATABASE_URL connects as app_runtime (not superuser) — verify if changing connection config
PhaseEstimated SessionsNotes
Phase 02–3 sessionsMostly config, fast with AI help
Phase 13–5 sessionsAuth has depth; worth going slow
Phase 22–4 sessionsConceptually dense; revisit often
Phase 34–6 sessionsThe learning payoff is highest here
Phase 42–4 sessionsTiptap is well-documented
Phase 54–8 sessionsRepetition; gets faster each sub-feature
Phase 66–10 sessionsMost technically complex phase
Phase 72–4 sessionsMostly config and testing
Phase 83–5 sessionsCLI is fun; API key crypto needs care
Phase 92–3 sessionsAudit work; methodical
Phase 102–3 sessionsSatisfying milestone
Phase 114–6 sessionsStripe webhooks need careful testing
Phase 12Open-endedExploratory; do when ready

Sessions are loosely defined as focused 2–3 hour working blocks. Estimates assume you’re reviewing every line and asking questions — that’s the point.

These items are known, intentional gaps — deferred, not forgotten.

B1 — Hard-Delete Erasure Job (GDPR / Account Deletion)

What: A scheduled or on-demand job that permanently erases user data rows after soft-delete, to satisfy GDPR right-to-erasure or account deletion requests.

Why deferred: No external users yet. Compliance obligation does not apply at MVP scale. Tombstones exist via soft-delete; the erasure step is not yet wired up.

Why it cannot use conventional channels:

  • DATABASE_URL connects as app_runtime — hard deletes are blocked by the BEFORE DELETE trigger.
  • DATABASE_DIRECT_URL is reserved for schema migrations — using it for runtime data operations conflates two distinct concerns.
  • createAdminClient().from(...).delete() bypasses RLS but not triggers — still rejected.

What it needs: A dedicated pathway that opens an explicit transaction, sets SET LOCAL app.allow_hard_delete = 'true', executes targeted DELETEs, and records an audit event.

Options to evaluate when the time comes:

  • Supabase pg_cron — a scheduled SQL job running inside the database itself, no external process needed
  • Edge Function with privileged connection — invoked on-demand via a secure internal endpoint, direct database access
  • Dedicated admin Drizzle client in packages/core scoped exclusively to erasure operations, distinct from the runtime db instance

When to implement: When onboarding external users, or when a compliance review requires a documented erasure process.