Arkive

Front Door: Row-Level Security Walkthrough

As of
Version1.1.0
Kindguidance
Length3,838 words, about 17 min
Tagsplanphase-1.1rlsdatabasesupabasedrizzlepostgresqlapitypescript

This guide walks through Phase 1.1 step by step. It assumes you have completed Phase 1: Supabase is connected, the profiles table exists, Drizzle migrations are working, and the basic RLS policy was created via the Supabase dashboard.

Every step explains what you are doing and why. Follow the reasoning, not just the commands.


Background — The Gap Phase 1.1 Addresses

After Phase 1, security enforcement existed at the application layer but not the database layer. Three concrete problems:

  1. Drizzle connected as the Postgres superuser. Superusers bypass all Row Level Security — every Drizzle query ran with unrestricted access, regardless of what RLS policies were defined.
  2. withRLS set a session variable outside a real transaction. set_config with is_local = true only scopes to the current transaction. Called outside a transaction block, there is no scope guarantee — the variable could persist across pooled connections and leak one user's ID into another user's request.
  3. No database-level enforcement for soft deletes. Hard deletes and updates on soft-deleted rows were blocked only by convention, not by the database. The service role (createAdminClient()) would bypass any RLS-based protection entirely.

Phase 1.1 closes all three gaps.


Task 1.1.1 — Understand Why Superuser Bypasses RLS

What and why: Before making changes, understand what you are fixing. This mental model will make every subsequent step obvious.

Postgres has three privilege layers:

  1. Superusers (postgres role, Supabase service role) — bypass everything: RLS policies, triggers that use SECURITY DEFINER, ownership checks. No exceptions.
  2. Table owners — bypass RLS by default, but you can force it with ALTER TABLE t FORCE ROW LEVEL SECURITY.
  3. Regular (non-superuser) roles — RLS is fully enforced.

Drizzle's DATABASE_URL in Phase 1 pointed to the Supabase pooler using the postgres superuser credentials. This means every query Drizzle ran was invisible to RLS — as if RLS did not exist.

The fix: create a non-superuser role (app_runtime) and switch DATABASE_URL to connect as that role. Once Drizzle connects as a regular role, all RLS policies are enforced for every runtime query.

Why not just rely on withRLS to protect everything? withRLS sets a session variable that RLS policies read. If Drizzle connects as a superuser, RLS policies are never evaluated — the session variable is set but never checked. The policy is bypassed entirely. Switching roles is what makes the policy actually run.

What about createAdminClient() and the service role? The Supabase service role is also a superuser — it bypasses RLS too. This is intentional for the admin client: it's used for trusted, elevated operations. But this means service role queries must also never be used for operations that should be user-scoped. Phase 1.1 introduces a second layer of enforcement for that: BEFORE triggers, which fire even for superusers.

Why can BEFORE triggers fire when RLS cannot? Triggers are procedural code bound to the table's DDL — they execute as part of the statement execution pipeline. Superuser status bypasses permission checks and row filtering, but not the statement execution pipeline itself. Triggers run regardless of who issued the query. This makes them the right mechanism for hard constraints (like prohibiting hard deletes) that must hold even for privileged callers.


Task 1.1.2 — Create the app_runtime PostgreSQL Role

What and why: app_runtime is a non-superuser role that Drizzle will use for all runtime queries. Creating the role is a one-time manual step in the Supabase SQL Editor — it must never be in a migration file because it requires a password, and the repository is public. Credentials in version control are a security incident.

Open Supabase Dashboard → SQL Editor → New query and run:

SQL
CREATE ROLE app_runtime WITH LOGIN PASSWORD 'choose-a-strong-password-here';

Save this password somewhere secure (your password manager). You will need it in Task 1.1.6 when updating .env.local.

Why not prefix with public.? PostgreSQL roles are cluster-level objects — they are not part of any schema. There is no public.app_runtime. The public. prefix is for tables and functions that live inside the public schema. Roles never take a schema prefix.

Why LOGIN and PASSWORD? The role needs to be able to open a database connection (LOGIN). Without LOGIN, the role exists but cannot authenticate. The password is the credential Supabase Supavisor will verify when Drizzle connects. If you forget LOGIN, the pooler will refuse the connection.

Verification: After running the statement, confirm the role was created:

SQL
SELECT rolname, rolcanlogin FROM pg_roles WHERE rolname = 'app_runtime';

You should see one row with rolcanlogin = true.


Task 1.1.3 — Run Migration 0001: Grant Permissions to app_runtime

What and why: A role with no grants can connect but cannot touch any tables. GRANT statements authorize app_runtime to read and write the tables it needs. These statements are safe to commit — they contain no secrets.

Create the file packages/core/src/db/migrations/0001_app_runtime_grants.sql:

SQL
GRANT USAGE ON SCHEMA public TO app_runtime;
GRANT SELECT, INSERT, UPDATE, DELETE ON ALL TABLES IN SCHEMA public TO app_runtime;
GRANT USAGE, SELECT ON ALL SEQUENCES IN SCHEMA public TO app_runtime;
ALTER DEFAULT PRIVILEGES IN SCHEMA public
  GRANT SELECT, INSERT, UPDATE, DELETE ON TABLES TO app_runtime;
ALTER DEFAULT PRIVILEGES IN SCHEMA public
  GRANT USAGE, SELECT ON SEQUENCES TO app_runtime;

Why GRANT USAGE ON SCHEMA public? Before a role can access any object inside a schema, it must have USAGE on the schema itself. Without this, even explicit table grants are ineffective — the role cannot resolve the schema path.

Why ALTER DEFAULT PRIVILEGES? GRANT ... ON ALL TABLES applies to tables that exist right now. Future tables (added by Phase 3+ migrations) would not be covered. ALTER DEFAULT PRIVILEGES tells Postgres: for every table created in this schema from this point forward, automatically grant these permissions to app_runtime. Without this, every new feature migration would need a manual GRANT or the app would get permission denied errors on new tables at runtime.

Why sequences? Auto-incrementing columns and DEFAULT gen_random_uuid() calls touch sequences. Without sequence grants, INSERT statements on tables with serial or UUID defaults would fail.

Now register this migration in the journal. Open packages/core/src/db/migrations/meta/_journal.json and add:

JSON
{
   "idx": 1,
   "version": "7",
   "when": 1778377618178,
   "tag": "0001_app_runtime_grants",
   "breakpoints": true
}

Critical: when must be strictly greater than the previous entry. Drizzle's migration runner orders migrations by when timestamp. If your new entry has a when value earlier than the 0000 migration's timestamp, Drizzle will silently skip it — it considers the migration already applied. Check 0000's when value and set yours to that_value + 1.

Apply the migration:

Shell
pnpm db:migrate

Verify the grants were applied:

SQL
SELECT grantee, table_name, privilege_type
FROM information_schema.role_table_grants
WHERE grantee = 'app_runtime';

You should see rows listing profiles (and any other tables) with SELECT, INSERT, UPDATE, DELETE privileges.

Why not db:generate first? db:generate reads your TypeScript schema files and generates SQL migrations from the difference between the schema and the current database state. It is for TypeScript-schema-driven changes. This migration contains raw SQL (GRANT statements) that Drizzle's schema introspection has no concept of. Running db:generate would be useless at best and harmful at worst — it might generate a conflicting file. Hand-written SQL migrations skip db:generate and go straight to db:migrate.


Task 1.1.4 — Run Migration 0002: Formalize the Profiles RLS Policy

What and why: The RLS policy from Phase 1 was created via the Supabase dashboard — it exists in the database but not in version control. This migration brings it under version control and upgrades it to the stronger policy pattern for Phase 1.1.

Phase 1's policy only filtered SELECT queries (it had no WITH CHECK clause). Phase 1.1 upgrades to FOR ALL with both USING and WITH CHECK, covering all statement types. It also adds FORCE ROW LEVEL SECURITY so the policy applies even to the table owner.

Create packages/core/src/db/migrations/0002_profiles_rls_policy.sql:

SQL
ALTER TABLE profiles ENABLE ROW LEVEL SECURITY;
ALTER TABLE profiles FORCE ROW LEVEL SECURITY;

DROP POLICY IF EXISTS "users_own_profile" ON profiles;
CREATE POLICY "users_own_profile"
  ON profiles FOR ALL
  USING  (id::text = current_setting('app.current_user_id', true))
  WITH CHECK (id::text = current_setting('app.current_user_id', true));

Register it in _journal.json:

JSON
{
   "idx": 2,
   "version": "7",
   "when": 1778377618179,
   "tag": "0002_profiles_rls_policy",
   "breakpoints": true
}

Apply:

Shell
pnpm db:migrate

Why FORCE ROW LEVEL SECURITY? Without FORCE, the table owner (the postgres superuser, which runs migrations) can bypass RLS. FORCE makes RLS apply to the table owner too. Superusers (the cluster-level postgres and Supabase's service role) still bypass it — FORCE does not change superuser behavior. But it closes the gap for table-owner bypasses.

Why FOR ALL instead of separate FOR SELECT policies? FOR ALL covers SELECT, INSERT, UPDATE, and DELETE in one policy. Using separate policies per statement type adds noise with no benefit when the predicate is identical for all operations.

What is USING vs WITH CHECK?

  • USING is evaluated for existing rows — it filters which rows a query can see or affect (SELECT, UPDATE, DELETE).
  • WITH CHECK is evaluated for new rows — it checks whether the row being written is allowed (INSERT, UPDATE).

Both use the same predicate here: the row's id must match the current user's ID.

Why does WITH CHECK allow deleted_at IS NOT NULL? When restoring a soft-deleted row (setting deleted_at = NULL), the write must be allowed. If WITH CHECK required deleted_at IS NULL, a restore UPDATE would fail. By omitting that condition from WITH CHECK, restores are allowed. USING still blocks reading deleted rows — SELECT won't return them.


Task 1.1.5 — Run Migration 0003: Define Shared Soft-Delete Trigger Functions

What and why: The architecture requires soft deletes on all syncable tables. Two invariants must hold even for the Supabase service role (superuser): (1) hard deletes are prohibited — use deleted_at instead; (2) updates on already-soft-deleted rows are prohibited — restore first. BEFORE triggers enforce both invariants for all callers, including superusers. The functions are defined once here; Phase 3 feature migrations bind them with CREATE TRIGGER per table.

Create packages/core/src/db/migrations/0003_soft_delete_trigger_fns.sql:

SQL
CREATE OR REPLACE FUNCTION enforce_soft_delete()
RETURNS trigger LANGUAGE plpgsql AS $$
BEGIN
  IF current_setting('app.allow_hard_delete', true) IS DISTINCT FROM 'true' THEN
    RAISE EXCEPTION
      'Hard deletes are prohibited on %. Use soft delete (set deleted_at).',
      TG_TABLE_NAME;
  END IF;
  RETURN OLD;
END;
$$;

CREATE OR REPLACE FUNCTION block_update_on_deleted()
RETURNS trigger LANGUAGE plpgsql AS $$
BEGIN
  IF OLD.deleted_at IS NOT NULL THEN
    RAISE EXCEPTION
      'Cannot update a soft-deleted row in % (id: %). Restore it first.',
      TG_TABLE_NAME, OLD.id;
  END IF;
  RETURN NEW;
END;
$$;

Register it in _journal.json:

JSON
{
   "idx": 3,
   "version": "7",
   "when": 1778377618180,
   "tag": "0003_soft_delete_trigger_fns",
   "breakpoints": true
}

Apply:

Shell
pnpm db:migrate

Why define the functions now instead of in Phase 3? Function definitions are table-independent. Defining them here means Phase 3 feature migrations only need two lines per table: CREATE TRIGGER no_hard_delete_notes BEFORE DELETE ON notes FOR EACH ROW EXECUTE FUNCTION enforce_soft_delete(). If you deferred the function definitions to Phase 3, each feature migration would need to carry the full function body — duplicated across every feature. Shared infrastructure belongs in core.

What is TG_TABLE_NAME? It is a special variable available inside PostgreSQL trigger functions. At runtime, it automatically resolves to the name of the table that fired the trigger. This allows one shared function to produce a useful error message regardless of which table it's bound to — no per-table customization needed.

What is app.allow_hard_delete? It is the admin escape hatch. For legitimate hard deletes (e.g., a GDPR erasure job), code can run SET LOCAL app.allow_hard_delete = 'true' inside a transaction. SET LOCAL scopes the setting to the current transaction and automatically clears it when the transaction ends. This is the only approved pathway for hard deletes.

IS DISTINCT FROM 'true' vs != 'true': If app.allow_hard_delete is not set, current_setting('app.allow_hard_delete', true) returns an empty string (not NULL — the true second argument suppresses the "setting not set" error). '' != 'true' is true — correct. But NULL != 'true' is NULL in Postgres — which is falsy, meaning the trigger would allow the delete. Using IS DISTINCT FROM handles both the empty string case and any future NULL case safely.

Why BEFORE and not AFTER triggers? BEFORE triggers can cancel the operation by returning NULL. AFTER triggers run after the row is already changed — they can still raise exceptions (which roll back the transaction), but the pattern is cleaner with BEFORE because the intent is to prevent the operation, not clean up after it.


Task 1.1.6 — Update DATABASE_URL to Connect as app_runtime

What and why: Grants are in place. Now switch Drizzle's runtime connection to use app_runtime instead of the superuser. This is the step that activates RLS enforcement for all Drizzle queries.

Open .env.local at the repo root (this file is gitignored — never commit it). Update DATABASE_URL to use app_runtime credentials:

Code
DATABASE_URL=postgresql://app_runtime.[project-ref]:[app_runtime-password]@aws-0-[region].pooler.supabase.com:6543/postgres

Find your pooler host and project ref in Supabase Dashboard → Project Settings → Database → Connection string (Transaction mode, port 6543). The path structure is the same as before — only the username and password change.

Leave DATABASE_DIRECT_URL unchanged. It still uses the superuser postgres credentials because Drizzle Kit migrations need superuser access to create tables, define policies, and grant permissions.

Why the pooler (port 6543) and not the direct connection? DATABASE_URL is used by your running application — serverless route handlers that create many short-lived connections. The pooler (Supabase Supavisor, port 6543) manages connection pooling and prevents exhausting Postgres's connection limit. The direct connection (port 5432) is for migrations, which need a stable, persistent connection for DDL transactions.

Why SET ROLE app_runtime does not work in the Supabase SQL Editor: Supabase restricts role switching in the dashboard SQL editor. If you try SET ROLE app_runtime, you will get ERROR 42501: permission denied to set role "app_runtime". This is a Supabase security restriction, not a problem with your setup. Use the system catalog queries in Task 1.1.8 to verify the role is configured correctly. True end-to-end verification is the running application.


Task 1.1.7 — Fix withRLS to Use a Real Transaction

What and why: Phase 1's withRLS called set_config('app.current_user_id', userId, true) outside a transaction. The true third argument means "local to the current transaction" — but without an explicit transaction, there is no transaction to scope to. The setting persists on the connection indefinitely. In Supabase Supavisor's transaction pooler (port 6543), connections are reused across requests. A leaked app.current_user_id from request A can persist when the same connection serves request B — the wrong user ID would be used for RLS filtering.

Open packages/core/src/db/rls.ts. The current implementation is:

TypeScript
export async function withRLS<T>(
   userId: string,
   fn: (db: typeof import("./index").db) => Promise<T>,
): Promise<T> {
   await db.execute(
      sql`select set_config('app.current_user_id', ${userId}, true)`,
   );
   return await fn(db);
}

Replace it with:

TypeScript
import { sql } from "drizzle-orm";
import { db } from "./";

type Tx = Parameters<Parameters<typeof db.transaction>[0]>[0];

export async function withRLS<T>(
   userId: string,
   fn: (tx: Tx) => Promise<T>,
): Promise<T> {
   return db.transaction(async (tx) => {
      await tx.execute(
         sql`select set_config('app.current_user_id', ${userId}, true)`,
      );
      return fn(tx);
   });
}

Update call sites to use tx instead of db inside the callback:

TypeScript
// Before
const profile = await withRLS(userId, async (db) => {
   return db.select().from(profiles).where(eq(profiles.id, userId));
});

// After — parameter name changes from db to tx; behaviour is identical
const profile = await withRLS(userId, async (tx) => {
   return tx.select().from(profiles).where(eq(profiles.id, userId));
});

Why does db.transaction() fix the scope issue? db.transaction() opens an explicit Postgres transaction. Inside a transaction, set_config with is_local = true is automatically cleared when the transaction commits or rolls back. The variable scope is now tied to the transaction lifetime, not the connection lifetime. When the transaction ends, the setting is gone — the connection is clean before it returns to the pool.

Why the Tx type alias? Drizzle's transaction callback receives a transaction object whose type is not exported directly. The Parameters<Parameters<typeof db.transaction>[0]>[0] pattern extracts the type from the function signature itself — it will stay correct if Drizzle's transaction API changes. Explicitly naming it Tx makes call sites readable.

Does wrapping every query in a transaction add overhead? Slightly. Postgres begins a transaction for every statement anyway — the overhead of an explicit BEGIN/COMMIT is minimal compared to the network round-trip. The security guarantee is worth it.


Task 1.1.8 — Clean Up the Stale Dashboard Policy

What and why: Migration 0002 ran DROP POLICY IF EXISTS "users_own_profile". But the original Phase 1 policy was created via the Supabase dashboard under a different name — "Users can manage their own profile". The DROP in the migration did not drop it (the name did not match). The profiles table now has two policies. Duplicate policies with identical predicates are harmless but confusing. Drop the old one manually.

In the Supabase SQL Editor:

SQL
DROP POLICY "Users can manage their own profile" ON profiles;

Verify only one policy remains:

SQL
SELECT policyname, cmd, qual, with_check
FROM pg_policies
WHERE tablename = 'profiles';

You should see one row: users_own_profile, command ALL, with a qual and with_check value showing the id::text = current_setting(...) predicate.

Why did the DROP in the migration not catch this? SQL DROP POLICY IF EXISTS matches on the exact policy name. The dashboard names policies differently depending on how you create them. This is a one-time manual cleanup; future policies will be created by migrations from the start and will not have this name mismatch.

What does the qual column show in pg_policies? qual is the USING clause — the predicate applied to existing rows. with_check is the WITH CHECK clause applied to written rows. Both should show (id)::text = current_setting('app.current_user_id'::text, true) after this cleanup.


Task 1.1.9 — Verify the Full Setup

What and why: Each component was verified independently. Now confirm the end-to-end chain is working: Drizzle connects as app_runtime, RLS filters queries correctly, and the application loads without errors.

Check role and grants exist

SQL
-- Role exists and can log in
SELECT rolname, rolcanlogin FROM pg_roles WHERE rolname = 'app_runtime';

-- Grants applied on profiles
SELECT grantee, table_name, privilege_type
FROM information_schema.role_table_grants
WHERE grantee = 'app_runtime' AND table_name = 'profiles';

-- One policy, correct predicate
SELECT policyname, cmd, qual FROM pg_policies WHERE tablename = 'profiles';

-- Trigger functions exist
SELECT proname FROM pg_proc
WHERE proname IN ('enforce_soft_delete', 'block_update_on_deleted');

Check the running application

Start the dev server:

Shell
pnpm dev

Log in and navigate to the dashboard. If the page loads and you see your data, app_runtime has the grants it needs and withRLS is setting the session variable correctly inside a transaction.

If you see a database error on load, the most likely cause is the app_runtime password in .env.local not matching what was set in CREATE ROLE. Double-check the credentials.

What you should now have

InvariantEnforcement mechanism
User A cannot read User B's dataRLS USING clause — enforced for app_runtime (non-superuser)
User ID cannot leak between pooled connectionsdb.transaction() wrapping — set_config is transaction-scoped
Hard deletes are prohibited on syncable tablesBEFORE DELETE trigger (fires for all roles including superuser)
Updates on soft-deleted rows are prohibitedBEFORE UPDATE trigger (fires for all roles including superuser)
Future tables automatically accessible to app_runtimeALTER DEFAULT PRIVILEGES in migration 0001

What Changed in packages/core

FileChange
src/db/rls.tsWrapped set_config in db.transaction() to properly scope the session variable
src/db/migrations/0001_app_runtime_grants.sqlNew: GRANT statements for app_runtime on existing and future tables
src/db/migrations/0002_profiles_rls_policy.sqlNew: FORCE ROW LEVEL SECURITY + formalised FOR ALL policy with WITH CHECK
src/db/migrations/0003_soft_delete_trigger_fns.sqlNew: shared trigger functions for hard-delete prevention and update-on-deleted prevention
src/db/migrations/meta/_journal.jsonThree new entries (idx 1, 2, 3) with strictly increasing when timestamps

.env.local (gitignored, not committed):

VariableChange
DATABASE_URLNow uses app_runtime credentials (non-superuser) instead of the postgres superuser
DATABASE_DIRECT_URLUnchanged — migrations still use the superuser

Going Further

Binding triggers to feature tables (Phase 3): When Phase 3 introduces notes and other syncable tables, each feature migration needs two CREATE TRIGGER lines:

SQL
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();

The trigger functions are already defined — you are just binding them to new tables.

Legitimate hard deletes (GDPR erasure, B1): If you ever need to hard-delete rows (e.g. a compliance erasure job), the only approved pathway is:

TypeScript
await db.transaction(async (tx) => {
   await tx.execute(sql`SET LOCAL app.allow_hard_delete = 'true'`);
   await tx.delete(notes).where(eq(notes.userId, userId));
});

SET LOCAL scopes the permission to the transaction. When the transaction commits, the setting clears automatically. Never use SET (session-scoped) for this.


Post note — Lock Out Supabase's Data API Roles (added 2026-10-23)

Sidekick found this gap at the end of Phase 2 lesson 3 and fixed it in sidekick#17 (migration 0008). If you're following along, do it here, at the end of Phase 1.1, as your next custom migration. Your migration number will differ; the end state is the same.

What and why: Supabase runs a REST API over the database (the Data API, PostgREST). A request with no login runs as the anon role, and one with a signed-in user's token runs as authenticated. Both are reachable with the public key the browser holds. Supabase's default privileges give both roles every privilege, TRUNCATE included, on each table postgres creates in public, and every function is executable by PUBLIC (all roles). RLS still gives them zero rows, because app.current_user_id is never set for them, but TRUNCATE ignores RLS, and one loosened policy would expose data. Sidekick never uses the Data API (all queries go through app_runtime and withRLS), so take the grants away and turn the API off.

  1. Check what the roles hold today, in the Supabase SQL Editor:
SQL
select grantee, table_name, privilege_type
from information_schema.role_table_grants
where table_schema = 'public' and grantee in ('anon', 'authenticated')
order by 1, 2, 3;
  1. Create the migration:
Shell
pnpm --filter @sidekick/core db:generate --custom --name revoke_data_api_roles
SQL
-- Existing tables and sequences
REVOKE ALL ON ALL TABLES IN SCHEMA public FROM anon, authenticated;
REVOKE ALL ON ALL SEQUENCES IN SCHEMA public FROM anon, authenticated;

-- Functions are executable by PUBLIC (every role) by default, so revoking from anon and
-- authenticated alone would change nothing. Triggers don't need EXECUTE to fire, and nothing
-- calls these functions directly, so revoke from PUBLIC too.
REVOKE EXECUTE ON ALL FUNCTIONS IN SCHEMA public FROM PUBLIC, anon, authenticated;

-- Tables, sequences and functions postgres creates from now on. Default privileges are kept
-- per creating role; migrations run as postgres, so this covers everything we create.
ALTER DEFAULT PRIVILEGES FOR ROLE postgres IN SCHEMA public
  REVOKE ALL ON TABLES FROM anon, authenticated;

ALTER DEFAULT PRIVILEGES FOR ROLE postgres IN SCHEMA public
  REVOKE ALL ON SEQUENCES FROM anon, authenticated;

ALTER DEFAULT PRIVILEGES FOR ROLE postgres IN SCHEMA public
  REVOKE ALL ON FUNCTIONS FROM PUBLIC, anon, authenticated;

service_role keeps its grants: the server-only admin client uses it, and it has BYPASSRLS anyway.

  1. Apply it with pnpm db:migrate, then re-run the query from step 1. It returns no rows.

  2. In the Supabase dashboard, turn the Data API off (Project Settings → Data API). Supabase Auth is a separate service, so sign-in keeps working.

Every later table (Phase 2's user_feature_entitlements included) is covered by the default privileges, so it never gets these grants. Pre-2's integration tests prove it; see the post note there.

Arkive

Index

Notes, decisions, and progress on my journey as I build Project Sidekick — built in the open; written as it happens.

Tags (137)

View

233 entries.

As ofTitleKind
2026-11-01Decisions Logdecisionlog
2026-11-01Synergy Checkpoint 3a: Prove the Store, Plan 3bplan phase-3 checkpoint guidanceplan
2026-11-01Synergy Lesson 1: Layer Folders and Lintplan phase-3 task monorepo eslintplan
2026-11-01Synergy Lesson 10: Save Call, Anchorsplan phase-3 task synergy apiplan
2026-11-01Synergy Lesson 11: Delete and Restoreplan phase-3 task synergy databaseplan
2026-11-01Synergy Lesson 12: Tagsplan phase-3 task synergy apiplan
2026-11-01Synergy Lesson 13: Race Testsplan phase-3 task synergy testing postgresqlplan
2026-11-01Synergy Lesson 2: Per-Package Migrationsplan phase-3 task database drizzle testingplan
2026-11-01Synergy Lesson 3: Entity Index and Version Logplan phase-3 task synergy database rls postgresqlplan
2026-11-01Synergy Lesson 4: Sandbox Providers and the First Routesplan phase-3 task synergy feature-system apiplan
2026-11-01Synergy Lesson 5: Outbox, Status and Versionsplan phase-3 task synergy database postgresqlplan
2026-11-01Synergy Lesson 6: Links, Relation Types and Cyclesplan phase-3 task synergy graph postgresql apiplan
2026-11-01Synergy Lesson 7: Staleness Dots and the Roll-Upplan phase-3 task synergy apiplan
2026-11-01Synergy Lesson 8: Lists and Supersedeplan phase-3 task synergy graph postgresqlplan
2026-11-01Synergy Lesson 9: Save Call, Mentionsplan phase-3 task synergy apiplan
2026-11-01Violations and Exceptionssystem-design synergyspec
2026-11-01Content Sequencesystem-design synergyspec
2026-11-01Tombstone Visibilitysystem-design synergy rlsspec
2026-11-01Anchors in Contentsystem-design synergy anchorsspec
2026-11-01Capture External Contentsystem-design synergy taxilaspec
2026-11-01Lexicon Highlightingsystem-design synergy taxilaspec
2026-11-01Mentions and Link Lifetimesystem-design synergyspec
2026-11-01Synergy Store Choicessystem-design synergy postgresqlspec
2026-11-01Synergy: System Designsystem-design synergyspec
2026-11-01Testing Layerssystem-design testing vitest playwrightspec
2026-11-01Synergy - Feature Specificationsprd spec synergyprd
2026-11-01Relationshipsprd feature synergyprd
2026-11-01Scope and Phasesprd feature synergyprd
2026-11-01Best Practice Rulessystem-design code-quality-check eslintspec
2026-11-01Stay on ESLint, Revisit Oxlintsystem-design eslint typescriptspec
2026-11-01Agent Guideai-coding agent gateway agent-guidance rls supabase drizzle api react prettierguidance
2026-11-01API Versioningsystem-design apispec
2026-11-01App Runtime for Drizzlesystem-design database drizzle postgresqlspec
2026-11-01Best Practicesai-coding-harness agent-guidance drizzle api react pnpm css offline embedding soft-deleteguidance
2026-11-01Centralized Copysystem-design contentspec
2026-11-01Corepacksystem-design monorepo pnpmspec
2026-11-01CSS Modulessystem-design css eslint mantinespec
2026-11-01Dependency Flowsystem-design monorepo clispec
2026-11-01dotenv CLIsystem-design config clispec
2026-11-01Edge Runtimesystem-design security supabasespec
2026-11-01Env Vars in Turbo JSONsystem-design monorepo turborepo vercelspec
2026-11-01ESLint Plugin Boundariessystem-design code-quality-check eslintspec
2026-11-01Flat Configsystem-design code-quality-check eslintspec
2026-11-01Force Dynamicsystem-design nextjs supabasespec
2026-11-01GraphQL and Relaysystem-design api graphql relayspec
2026-11-01Installation at Workspace Levelsystem-design code-quality-check typescript pnpmspec
2026-11-01Module Resolution: Bundlersystem-design code-quality-check typescriptspec
2026-11-01Prettier at Repo Levelsystem-design code-quality-check prettierspec
2026-11-01Profile Creation Postgres Triggersystem-design security postgresql api authenticationspec
2026-11-01Public API Onlysystem-design api rls authentication clispec
2026-11-01Runtime Validationssystem-design typescriptspec
2026-11-01Shared tsconfigsystem-design code-quality-check typescriptspec
2026-11-01Synergyplan living-plan phase-3 synergy rls postgresql api graphplan
2026-11-01tsupsystem-design code-quality-check eslintspec
2026-11-01Turboreposystem-design monorepo turborepo pnpmspec
2026-11-01Vercelsystem-design monorepo turborepo pnpm vercelspec
2026-10-31Entities and the Entity Indexprd feature synergyprd
2026-10-31Foundationsprd feature synergyprd
2026-10-31Keeping Links Trueprd feature synergyprd
2026-10-31Sneak Peek Linkprd feature synergyprd
2026-10-30Search and Backlinksprd feature synergyprd
2026-10-30Tagsprd feature synergyprd
2026-10-29Cost to Add Laterprinciple product engineeringguidance
2026-10-27Individual Firstprinciple productguidance
2026-10-27One Interfaceprinciple api cliguidance
2026-10-27Overview of Synergyprd feature synergyprd
2026-10-24Gatekeeper Lesson 7 Walkthrough: The Ops Package and Entitlements Grantbuild walkthrough phase-2 feature-system database postgresql cliguidance
2026-10-24Gatekeeper Lesson 8: Close-Outplan phase-2 task guidanceplan
2026-10-24Gatekeeper Walkthroughbuild walkthrough phase-2 api-guard feature-system rls authenticationguidance
2026-10-24Gatekeeper Lesson 3 Walkthrough: Entitlementsbuild walkthrough phase-2 feature-system database drizzle rls postgresql testingguidance
2026-10-24Gatekeeper Lesson 4 Walkthrough: withApiGuardbuild walkthrough phase-2 api-guard rls api authentication testingguidance
2026-10-24Gatekeeper Lesson 5 Walkthrough: Request and Auth-Failure Loggingbuild walkthrough phase-2 api-guard observability eslint testingguidance
2026-10-24Gatekeeper Lesson 6 Walkthrough: /api/health and Guard Verificationbuild walkthrough phase-2 api-guard api nextjs testingguidance
2026-10-24Gatekeeper Lesson 2 Walkthrough: resolveApiCallerbuild walkthrough phase-2 api-guard authentication supabase testingguidance
2026-10-24Gatekeeper Build Rules and Target Designplan phase-2 api-guard rls api authentication typescript guidanceplan
2026-10-24Gatekeeper Lesson 1 Walkthrough: Feature Registrybuild walkthrough phase-2 feature-system typescript monorepo testingguidance
2026-10-24Gatekeeper Lesson 7 (Task 2.10): Dev Entitlement Seed Scriptplan phase-2 task feature-system database postgresqlplan
2026-10-24Front Door: Hardening Walkthroughbuild walkthrough pre-2 rls supabase drizzle postgresql testing eslintguidance
2026-10-24Backlogged / Unplannedplan living-plan backlog feature-system taxila zinsser alter-ego parrot rls supabaseplan
2026-10-24Commands and Environmentai-coding command environment agent-guidance prettier pnpm cliguidance
2026-10-24Gatekeeperplan living-plan phase-2 api-guard rls api authentication feature-systemplan
2026-10-24Living Implementation Planplan living-plan taxila zinsser alter-ego war-room factory parrot core-driveplan
2026-10-24Terminalplan living-plan phase-9 taxila api-guard rls api authentication cliplan
2026-10-24With API Guardsystem-design security api-guard rls api authentication typescriptspec
2026-10-23Gatekeeper Lesson 3 (Tasks 2.8, 2.9): Entitlements Table and getEnabledFeaturesplan phase-2 task feature-system database drizzle rls postgresql testingplan
2026-10-23Gatekeeper Lesson 4 (Tasks 2.2, 2.3): withApiGuard With withRLSplan phase-2 task api-guard rls api security testingplan
2026-10-23Gatekeeper Lesson 5 (Tasks 2.4, 2.5): Request and Auth-Failure Loggingplan phase-2 task api-guard observability eslintplan
2026-10-23Gatekeeper Lesson 6 (Tasks 2.11, 2.12): /api/health and Guard Verificationplan phase-2 task api-guard api nextjs testingplan
2026-10-23Centralizedsystem-design observability api authenticationspec
2026-10-23Database and Securityai-coding database security agent-guidance api-guard rls drizzle api react repository-patternguidance
2026-10-23Error Handlingsystem-design observability api-guard rls supabase api nextjs react aispec
2026-10-23Front Door: Row-Level Security WalkthroughReadingplan phase-1.1 rls database supabase drizzle postgresql api typescriptguidance
2026-10-23Groundwork Checkpoint B Walkthroughbuild walkthrough phase-0 checkpointguidance
2026-10-20Gatekeeper Lesson 2 (Task 2.1): resolveApiCallerplan phase-2 task api-guard authentication supabase nextjsplan
2026-10-18Gatekeeper Lesson 1 (Tasks 2.6, 2.7): FeatureManifest and ALL_FEATURESplan phase-2 task feature-system typescript monorepoplan
2026-10-18Registrysystem-design feature-systemspec
2026-10-17Core Drive to Be Practical and Grounded in Realitysystem-design feature core-drive prdprd
2026-10-17MVP Scopeprd mvp specification core-drive taxila war-room factory alter-ego zinsser parrot api cli native-appprd
2026-10-17An AI Writing Coach and an Appsystem-design prd feature zinsser aiprd
2026-10-17Overview of Taxilasystem-design prd feature taxila ragprd
2026-10-16Front Door: Hardeningplan pre-2 foundation security database testingplan
2026-10-16Server Actionssystem-design nextjs rls api authenticationspec
2026-10-14Profiles Schemasystem-design security rls authenticationspec
2026-10-14Tag Taxonomyai-coding agent-guidance taxonomy discoveryguidance
2026-10-13Anatomy of a Core Drive Entrysystem-design prd feature core-driveprd
2026-10-13Core Drive Stores Personal and Sensitive Informationsystem-design prd feature core-drive taxila graphprd
2026-10-13Core Drive Stores Values Onlysystem-design prd feature core-drive war-room factoryprd
2026-10-13Core Drive to Foster Reflections and Learningssystem-design prd feature core-driveprd
2026-10-13Groundwork: Checkpointsplan phase-0 api nextjs typescript eslint turborepo pnpmplan
2026-10-13Model Routingprd vercel ai embedding routingprd
2026-10-13Overview of Core Drivesystem-design prd feature core-drive factory ai alter-egoprd
2026-10-13Source Attributionprd feature alter-ego aiprd
2026-10-12Command Paletteprd ux specificationprd
2026-10-12MVP Design Objectiveprd ux specificationprd
2026-10-12Be the Best Version of Yourselfprd value-proposition specificationprd
2026-10-12Make the Right Behavior Easierprd value-proposition specificationprd
2026-10-11Solve Problems and Build Thingsmotivation creativityjournal
2026-10-11A Personal Discoverymotivation discipline bucket-list ambition entrepreneurship sidekickjournal
2026-10-11Commercialize Sidekickmotivation finance freedomjournal
2026-10-10Realityprinciple reality pragmatism survivalguidance
2026-10-10Seek Closureprinciple productivity happiness purposeguidance
2026-10-08No Do-Oversprinciple core-drive execution productivityguidance
2026-10-08Pragmatismprinciple practical pragmaticguidance
2026-10-04Protect Your Mind and Your Timeprinciple productivity automation minimalismguidance
2026-10-03Focus Funnelprinciple core-drive productivityguidance
2026-10-02Fuel Career Growthmotivation career ambitionjournal
2026-10-02Learnmotivation learning ai ragjournal
2026-09-19Actual Stateglossary terminologyglossary
2026-09-19Algorithm of Lifeglossary terminology nextjsglossary
2026-09-19Alter Egoplan living-plan phase-6 taxila zinsser alter-ego war-room factory core-driveplan
2026-09-19Alter Egosystem-design prd feature taxila alter-ego factory aiprd
2026-09-19API Firstsystem-design api clispec
2026-09-19Approve Buildssystem-design monorepo nextjs pnpmspec
2026-09-19Architectural Driverssystem-design offlinespec
2026-09-19Architecture Invariantsai-coding architecture invariant agent-guidance api nextjsguidance
2026-09-19Asynchronoussystem-design rag embeddingspec
2026-09-19Atomicsystem-design rag embeddingspec
2026-09-19Authenticationsystem-design security taxila api authentication pwa mobile clispec
2026-09-19Background Jobssystem-design vercel background-jobspec
2026-09-19Build for Today, Designed for Futuresystem-design offlinespec
2026-09-19Bundle Everythingsystem-design monorepospec
2026-09-19Capacitorsystem-design native-app nextjs mobile offlinespec
2026-09-19Centralizedsystem-design feature-systemspec
2026-09-19Centralized and Client-Agnosticsystem-design security rls api authentication clispec
2026-09-19CLIsystem-design cli apispec
2026-09-19Context Assemblersystem-design ai war-room factory api-guardspec
2026-09-19Coresystem-design feature-systemspec
2026-09-19Core Drive and Taxilasystem-design prd feature taxila core-driveprd
2026-09-19Driving Forces and Key Featuressystem-design api offlinespec
2026-09-19Embedding Status Fieldsystem-design rag embedding typescriptspec
2026-09-19Enforced Conventionssystem-designspec
2026-09-19Essential and Ideal Stateglossary terminologyglossary
2026-09-19Evolvabilitysystem-design rls offlinespec
2026-09-19Factorysystem-design prd feature factoryprd
2026-09-19Feature Package Workai-coding feature monorepo agent-guidance feature-systemguidance
2026-09-19Front Doorplan living-plan phase-1 api-guard rls supabase drizzle postgresql apiplan
2026-09-19Front Door Task 1.1plan phase-1 task supabase postgresql api authentication nextjsplan
2026-09-19Front Door Task 1.10plan phase-1 task rls supabase drizzle postgresql authentication typescriptplan
2026-09-19Front Door Task 1.11plan phase-1 task api-guard rls supabase api authentication nextjsplan
2026-09-19Front Door Task 1.14plan phase-1 task react pnpm mantine cssplan
2026-09-19Front Door Task 1.17plan phase-1 task supabase authenticationplan
2026-09-19Front Door Task 1.18plan phase-1 task api-guard rls supabase drizzle postgresql authenticationplan
2026-09-19Front Door Task 1.2plan phase-1 task supabase drizzle postgresql authentication nextjs pnpmplan
2026-09-19Front Door Task 1.3plan phase-1 task supabase nextjs typescriptplan
2026-09-19Front Door Task 1.4plan phase-1 task supabase authentication nextjs react typescriptplan
2026-09-19Front Door Task 1.5plan phase-1 task rls supabase authentication typescriptplan
2026-09-19Front Door Task 1.6plan phase-1 task rls supabase drizzle authentication typescript offlineplan
2026-09-19Front Door Task 1.7plan phase-1 task drizzle postgresql typescriptplan
2026-09-19Front Door Task 1.8plan phase-1 task turborepo pnpmplan
2026-09-19Front Door Task 1.9plan phase-1 task rls supabase drizzle postgresql authenticationplan
2026-09-19Front Door Tasks 1.12 and 1.13plan phase-1 task supabase api authentication nextjs react typescriptplan
2026-09-19Front Door Tasks 1.15 and 1.16plan phase-1 task supabase authentication nextjs react mantineplan
2026-09-19Front Door: Original Planplan phase-1 rls supabase drizzle postgresql authentication nextjsguidance
2026-09-19Front Door: Row-Level Securityplan living-plan phase-1.1 rls supabase drizzle postgresql pnpm soft-deleteplan
2026-09-19Groundworkplan living-plan phase-0 nextjs typescript eslint prettier turborepo pnpmplan
2026-09-19Groundwork Checkpoint Aplan phase-0 checkpoint nextjs typescript turborepo pnpm cliplan
2026-09-19Groundwork Checkpoint Bplan phase-0 checkpoint eslint prettier turborepo pnpmplan
2026-09-19Groundwork Checkpoint Cplan phase-0 checkpoint nextjs pnpmplan
2026-09-19Groundwork Checkpoint Dplan phase-0 checkpoint api-guard rls supabase drizzle api authenticationplan
2026-09-19Historic Stateglossary terminologyglossary
2026-09-19Idempotent APIsystem-design api idempotencyspec
2026-09-19Independent Deploymentssystem-design feature-systemspec
2026-09-19Informationglossary terminologyglossary
2026-09-19Instrumentationsystem-design observability vercel embeddingspec
2026-09-19Isolationsystem-design feature-systemspec
2026-09-19Lifecycle and Traceabilityprd governance planningguidance
2026-09-19Mantinesystem-design css mantinespec
2026-09-19Many Clients and Many Consumerssystem-design api pwa ai rag clispec
2026-09-19May Become Part of Taxilasystem-design core-drive taxilaspec
2026-09-19Middlewaresystem-design security api authenticationspec
2026-09-19Migrationsystem-design database drizzle pnpmspec
2026-09-19Model Router Contractsystem-design ai model-routing provider-agnostic embeddingspec
2026-09-19Never Mutate Outside API Layersystem-design apispec
2026-09-19Non-Syncable Tablessystem-design database offlinespec
2026-09-19Notesai-coding-harness agent-guidance nextjs eslint pnpm cssguidance
2026-09-19Observabilitysystem-design api-guard api authentication vercel observabilityspec
2026-09-19Observablesystem-design rag embedding observabilityspec
2026-09-19Offline Readysystem-design api pwa offline soft-delete idempotencyspec
2026-09-19Open Houseplan living-plan phase-11plan
2026-09-19Parrotsystem-design prd feature parrotprd
2026-09-19Pocketplan living-plan phase-8 taxila alter-ego nextjs vercel pwa mobileplan
2026-09-19Prefer JSON Configsystem-design monorepospec
2026-09-19Present Stateglossary terminologyglossary
2026-09-19Productivity Modulesystem-design prd feature war-roomprd
2026-09-19PWAsystem-design pwa offlinespec
2026-09-19RAGsystem-design rag ai embeddingspec
2026-09-19Repository Clientsystem-design database rls supabase drizzle api repository-patternspec
2026-09-19Request Flowsystem-design monorepo api offlinespec
2026-09-19Retryablesystem-design rag embeddingspec
2026-09-19Row-Level Securitysystem-design security rls drizzle offlinespec
2026-09-19Soft-Delete Supportsystem-design database soft-deletespec
2026-09-19Start Todayprinciple core-drive execution consistencyguidance
2026-09-19State Representationsystem-design api aispec
2026-09-19Storefrontplan living-plan phase-12 api offlineplan
2026-09-19Structured Response Attributionsystem-design ai structured-streaming source-attribution citationspec
2026-09-19Syncable Tablessystem-design database offline soft-deletespec
2026-09-19System Overview Diagramsystem-designspec
2026-09-19Taxilaplan living-plan phase-4 taxila api-guard rls drizzle api authenticationplan
2026-09-19Technology Stacksystem-design supabase drizzle postgresql nextjs typescript turborepo pnpm vercelspec
2026-09-19Tiered Context Modelsystem-design core-drive domain situation war-room factory ai embeddingspec
2026-09-19Tiptap Requirementsystem-design rag embeddingspec
2026-09-19Use Navigation Hooksystem-design nextjsspec
2026-09-19War Roomplan living-plan phase-7 war-room factory core-drive rls api offlineplan
2026-09-19Watchtowerplan living-plan phase-10 api-guard rls api authentication nextjs offlineplan
2026-09-19Zinsserplan living-plan phase-5 taxila zinsser api mantine ai embeddingplan
2026-08-24Architectural OverviewSidekick's architectural overview — an API-first platform tuned for solo developerssidekick project technical architecturedocument
2026-07-19Project Sidekick - A Bird’s Eye ViewAn introduction to Project Sidekick. What is it? What problem does it solve? Why am I building it?sidekick project essaydocument
2026-07-10Building in the Margins of RealityProject kick-off post and a confessional essay about my past attempts and their failures. Why start now? What do I hope to achieve?sidekick project essayjournal
2026-05-29Config Files Explainedbuild walkthrough phase-0 turborepo typescript eslint prettierguidance
2026-05-29Front Door: Original Walkthroughbuild walkthrough phase-1 supabase drizzle authenticationguidance
2026-05-29Groundwork Checkpoint A Walkthroughbuild walkthrough phase-0 checkpointguidance
2026-05-29Groundwork Checkpoint C Walkthroughbuild walkthrough phase-0 checkpointguidance
2026-05-29Groundwork Checkpoint D Walkthroughbuild walkthrough phase-0 checkpointguidance