Arkive

Synergy Lesson 3 Walkthrough: Entity Index and Version Log

As of
Version1.0.0
Kindguidance
Length4,176 words, about 18 min
Tagsbuildwalkthroughphase-3synergydatabaserlspostgresql

This guide reproduces lesson 3 of Phase 3 step by step. Synergy gets its first tables, entity_index and version_log, locked down with row-level security that compares only user_id, the soft-delete triggers and a strict restore. Every Synergy write takes a per-user advisory lock, enforced by a lint rule, and the first repository functions register, save and read an entity. It implements the Synergy overview (Tables, Tombstones, Write path), Content sequence and Tombstone visibility, and writes the guide to delete models.

It shipped as sidekick#58, one squash-merged PR, after an unplanned step that shipped as sidekick#55. What was learned along the way is in the notes.

Run everything from the Sidekick repo root unless a step says otherwise.

Where the build differs from the brief

  • A step before the lesson: package.json files are sorted by Prettier (step 0, sidekick#55). Pratik's editor kept reordering core's package.json on save; a Prettier plugin now owns the order.
  • The actor kind is human, not person. It reads as the opposite of agent, and user already means the owner (user_id).
  • Strict restore. The lesson 2 handover left open whether a restore may also edit. It may not: the trigger refuses a restore that changes anything but deleted_at and the provenance columns (D263, step 6b).
  • The lock is a wrapper with a lint rule. The brief asked for a helper every write calls first. Pratik made it structural: withUserLock(write) returns the locked function, the bare lock function isn't exported, and the new rule sidekick/require-user-lock fails any insert, update or delete in Synergy's repository that isn't inside the wrapper (D265, step 5).
  • The lock key comes from the session, not a parameter. hashtextextended(current_setting('app.current_user_id'), 0), the user withRLS set, so no caller can lock one user and write as another, and a write outside withRLS fails loudly.
  • Keys are text with a CHECK, not Postgres enums (D266, step 1).
  • Two unapplied migrations were regenerated. A typo in the policy SQL (step 3a) and a core migration generated into the wrong package (step 7) were removed and regenerated before anything was applied. Step 3's gotcha explains how.

Step 0 — Sort package.json with Prettier (before the lesson)

What and why: an editor's sort-on-save reordered core's package.json keys on every save, so diffs carried noise. prettier-plugin-packagejson gives every package.json one canonical order, applied by the same Prettier that pre-commit and CI already run.

Shell
pnpm add -D -w prettier-plugin-packagejson@^3.1.0

In .prettierrc.json, add it to plugins, last:

JSON
"plugins": [
   "prettier-plugin-astro",
   "@ianvs/prettier-plugin-sort-imports",
   "prettier-plugin-packagejson"
],
Shell
pnpm format

Every package.json is rewritten once, in the plugin's order. Shipped as sidekick#55.

Step 1 — Design the tables

Shell
pnpm add drizzle-orm@^0.45.4 --filter @sidekick/synergy

1a — Columns every Synergy table shares

What and why: the overview gives every Synergy table the same timestamps, provenance and soft-delete columns. Written once and spread into each pgTable, they can't drift between the ten tables to come.

packages/platform/synergy/src/db/schema/columns.ts:

TypeScript
import { sql, type SQL } from 'drizzle-orm';
import {
   check,
   text,
   timestamp,
   uuid,
   type AnyPgColumn,
   type CheckBuilder,
} from 'drizzle-orm/pg-core';

// Columns every Synergy table carries (Synergy overview, "Every table").
// Spread them into each pgTable: Drizzle builds a fresh column per table.

export function utcTimestamp(name: string): ReturnType<typeof timestamp> {
   return timestamp(name, { withTimezone: true });
}

// Renders our own constants as a SQL list: 'a', 'b'. sql.raw pastes text
// unescaped, so never pass it anything a user typed.
export function sqlList(values: readonly string[]): SQL {
   return sql.raw(values.map((value) => `'${value}'`).join(', '));
}

export const timestamps = {
   createdAt: utcTimestamp('created_at').notNull().defaultNow(),
   updatedAt: utcTimestamp('updated_at').notNull().defaultNow(),
};

// NULL while the row is live. A soft-deleted row (a tombstone) keeps its data.
export const tombstone = {
   deletedAt: utcTimestamp('deleted_at'),
};

// Who made the row's last write, and why. Keys only (D201).
export const ACTOR_KINDS = ['human', 'system', 'agent', 'api_key'] as const;

export const provenance = {
   actorKind: text('actor_kind', { enum: ACTOR_KINDS }).notNull(),
   actorId: uuid('actor_id'),
   reason: text('reason'),
};

// The database's half of the provenance contract. Call it in each table's
// third pgTable argument: ...provenanceChecks('entity_index', self)
export function provenanceChecks(
   table: string,
   self: { actorKind: AnyPgColumn; actorId: AnyPgColumn },
): [CheckBuilder, CheckBuilder] {
   return [
      check(
         `${table}_actor_kind_check`,
         sql`${self.actorKind} IN (${sqlList(ACTOR_KINDS)})`,
      ),
      check(
         `${table}_actor_id_check`,
         sql`(${self.actorKind} = 'system') = (${self.actorId} IS NULL)`,
      ),
   ];
}
  • Text and a CHECK, not a Postgres enum (D266). An enum value can be added but never removed or renamed without rebuilding the type, and ALTER TYPE … ADD VALUE can't be used in the transaction that adds it, which is every drizzle-kit migration batch. The { enum } option types the column in TypeScript; the CHECK guards the data from every other writer.
  • sqlList keeps one source of truth. Interpolating a value in Drizzle's sql makes it a bound parameter, and a CHECK is stored schema that can't take one. ACTOR_KINDS.join(',') would also have been one string, 'human,system,agent,api_key', so every insert would have failed.
  • Provenance records the last write. History lives in version_log and, from lesson 5, the outbox. (actor_kind = 'system') = (actor_id IS NULL) compares two booleans: only the system acts without an id.
  • updated_at has no trigger. The repository sets it with the database's now() on every write.

1b — entity_index

packages/platform/synergy/src/db/schema/entity-index.ts (as merged, with step 3a's policy line):

TypeScript
import { sql } from 'drizzle-orm';
import {
   check,
   index,
   integer,
   pgTable,
   text,
   uuid,
} from 'drizzle-orm/pg-core';

import { profiles } from '@sidekick/core/db/schema';

import {
   provenance,
   provenanceChecks,
   sqlList,
   timestamps,
   tombstone,
   utcTimestamp,
} from './columns';
import { ownRowsPolicy } from './policies';

// Delete model: soft delete, tombstones visible (D66). RLS compares only
// user_id; the repository filters deleted_at in each query that wants live rows.

export const ENTITY_STATUSES = ['active', 'deprecated', 'superseded'] as const;

export const entityIndex = pgTable(
   'entity_index',
   {
      // Client-generated, so the editor knows the id before the first save.
      id: uuid('id').primaryKey(),
      userId: uuid('user_id')
         .notNull()
         .references(() => profiles.id, { onDelete: 'cascade' }),
      // Keys, never labels (D201): 'sandbox-notes', 'note'.
      provider: text('provider').notNull(),
      type: text('type').notNull(),
      // User content, stored as written.
      displayName: text('display_name').notNull(),
      title: text('title'),
      status: text('status', { enum: ENTITY_STATUSES })
         .notNull()
         .default('active'),
      version: integer('version').notNull().default(1),
      revision: integer('revision').notNull().default(0),
      // Moves on every content write, anchors included: the stale-write token.
      contentSeq: integer('content_seq').notNull().default(0),
      editedAt: utcTimestamp('edited_at').notNull().defaultNow(),
      ...timestamps,
      ...provenance,
      ...tombstone,
   },
   (self) => [
      index('entity_index_user_id_status_idx').on(self.userId, self.status),
      check(
         'entity_index_status_check',
         sql`${self.status} IN (${sqlList(ENTITY_STATUSES)})`,
      ),
      check(
         'entity_index_counters_check',
         sql`${self.version} > 0 AND ${self.revision} >= 0 AND ${self.contentSeq} >= 0`,
      ),
      ...provenanceChecks('entity_index', self),
      ownRowsPolicy('entity_index', self),
   ],
);
  • provider and type have no CHECK. Statuses and actor kinds are a small fixed set Synergy's code acts on; providers and types are an open set other packages register. Lesson 4's registry validates them in the repository, so Synergy never names a provider.
  • "Deleted" is not a status. It's deleted_at, so a restore brings back whatever status the entity had.
  • edited_at moves only with author content (title, body, blurb, with the version or revision); updated_at moves on every write, status changes and anchors included.
  • Counters start at version 1, revision 0, content_seq 0, and are integers: bigint comes back from the driver as a string.

1c — version_log and the schema index

packages/platform/synergy/src/db/schema/version-log.ts:

TypeScript
import { sql } from 'drizzle-orm';
import {
   check,
   integer,
   pgTable,
   text,
   uniqueIndex,
   uuid,
} from 'drizzle-orm/pg-core';

import { profiles } from '@sidekick/core/db/schema';

import { provenance, provenanceChecks, timestamps, tombstone } from './columns';
import { entityIndex } from './entity-index';
import { ownRowsPolicy } from './policies';

// Delete model: soft delete, tombstones visible (D66), like entity_index.
// One row per version bump, written by saveEntityContent in the same transaction.

export const versionLog = pgTable(
   'version_log',
   {
      // Written by Synergy on the server, never by an offline client.
      id: uuid('id').primaryKey().defaultRandom(),
      userId: uuid('user_id')
         .notNull()
         .references(() => profiles.id, { onDelete: 'cascade' }),
      entityId: uuid('entity_id')
         .notNull()
         .references(() => entityIndex.id, { onDelete: 'cascade' }),
      version: integer('version').notNull(),
      // Optional and stored as written: user content.
      changeNote: text('change_note'),
      ...timestamps,
      ...provenance,
      ...tombstone,
   },
   (self) => [
      // Partial: a tombstoned row never blocks logging the same version again.
      uniqueIndex('version_log_entity_id_version_key')
         .on(self.entityId, self.version)
         .where(sql`${self.deletedAt} IS NULL`),
      check('version_log_version_check', sql`${self.version} > 0`),
      ...provenanceChecks('version_log', self),
      ownRowsPolicy('version_log', self),
   ],
);

src/db/schema/index.ts:

TypeScript
// Synergy's Drizzle tables. drizzle-kit reads this file to generate
// migrations, so every table must be exported from here.
export * from './entity-index';
export * from './version-log';
  • The log records bumps, not creation. Version 1 has no note and nothing reads a row for it; creation is in created_at and, from lesson 5, the outbox.
  • A surrogate id, not the composite key (entity_id, version). A primary key can't be partial, so a tombstoned version 3 would block logging version 3 again, and every Synergy table names its rows by one id. The natural key is still enforced, by the partial unique index.
  • defaultRandom() because only Synergy writes these rows, inside the save; client-generated ids are for rows a client creates and refers to before the server sees them.
  • Its own user_id so the policy reads the row's own column instead of joining entity_index.

Step 2 — Generate and read the SQL

Shell
pnpm db:generate --filter=@sidekick/synergy -- --name=entity_index_and_version_log

0001_entity_index_and_version_log.sql creates both tables with their CHECKs inline, then adds the foreign keys as separate ALTER TABLE statements (drizzle-kit creates every table first), then the indexes. Columns inside CHECKs and the index predicate appear as "entity_index"."status"; Postgres accepts that and strips the table name when it stores them. The profiles foreign key works because core's migrations run before Synergy's (lesson 2). Nothing is migrated yet: until step 3, these tables have no RLS.

Step 3 — Lock it down

3a — The own-rows policy

What and why: a policy is a condition Postgres adds to every query on the table. USING decides which rows a query can see (a failing row is invisible); WITH CHECK decides which rows it may write (a failing row is an error). Synergy's compares only user_id (D66); the repository filters deleted_at itself. Written as a Drizzle pgPolicy helper, drizzle-kit tracks the policy in its snapshot, so a later change to the helper regenerates every table's policy.

packages/platform/synergy/src/db/schema/policies.ts:

TypeScript
import { sql } from 'drizzle-orm';
import { pgPolicy, type AnyPgColumn, type PgPolicy } from 'drizzle-orm/pg-core';

// Synergy's row-level security: a user reads and writes only their own rows.
// It compares only user_id, never deleted_at: tombstones stay visible to the
// repository, which filters deleted rows itself (D66).
export function ownRowsPolicy(
   tableName: string,
   self: { userId: AnyPgColumn },
): PgPolicy {
   // nullif: once a withRLS transaction ends, its pooled connection reads the
   // setting back as '' (not NULL), and ''::uuid would throw.
   const ownRow = sql`${self.userId} = nullif(current_setting('app.current_user_id', true), '')::uuid`;

   return pgPolicy(`${tableName}_own_rows`, {
      for: 'all',
      using: ownRow,
      withCheck: ownRow,
   });
}

Add ownRowsPolicy('entity_index', self) and ownRowsPolicy('version_log', self) to the tables (shown in step 1), then:

Shell
pnpm db:generate --filter=@sidekick/synergy -- --name=own_rows_policies

0002_own_rows_policies.sql enables RLS and creates one policy per table. Comparing uuid to uuid, not core's id::text = …, lets the (user_id, status) index serve the filter.

Gotcha — a migration is three files. The first version of this step lost the quotes around 'app.current_user_id'; Postgres would have read it as a column of a table named app and refused the migration (and, in the same transaction, 0001). Deleting only the .sql left its meta/0002_snapshot.json and its journal entry: drizzle-kit generates from the newest snapshot, so the regenerate wrote an ALTER POLICY fix on top of a policy that no longer existed, and db:migrate would have looked for the missing file. To remove an unapplied migration, delete the .sql, its snapshot and its _journal.json entry together. Never remove an applied one.

3b — FORCE and the triggers

Shell
pnpm db:generate --filter=@sidekick/synergy -- --custom --name=force_rls_and_soft_delete_triggers

0003_force_rls_and_soft_delete_triggers.sql:

SQL
-- Synergy's tables keep their tombstones visible (D66): 0002's policies compare
-- only user_id, and these triggers hold the soft-delete rules for every writer.

-- FORCE: the policies bind the table owner too. If app_runtime ever owned these
-- tables, RLS would still hold. Roles with BYPASSRLS (postgres, for migrations
-- and ops scripts) still bypass it.
ALTER TABLE entity_index FORCE ROW LEVEL SECURITY;
ALTER TABLE version_log FORCE ROW LEVEL SECURITY;

-- Soft delete only: a DELETE is refused unless its transaction opts in with
-- SET LOCAL app.allow_hard_delete = 'true' (erasure). Core's shared function.
CREATE TRIGGER no_hard_delete_entity_index
   BEFORE DELETE ON entity_index
   FOR EACH ROW EXECUTE FUNCTION enforce_soft_delete();

CREATE TRIGGER no_hard_delete_version_log
   BEFORE DELETE ON version_log
   FOR EACH ROW EXECUTE FUNCTION enforce_soft_delete();

-- A tombstone can be restored, but not otherwise updated. Synergy's own
-- function (0000), not core's block_update_on_deleted, which refuses restores.
CREATE TRIGGER no_update_deleted_entity_index
   BEFORE UPDATE ON entity_index
   FOR EACH ROW EXECUTE FUNCTION synergy_block_update_on_deleted();

CREATE TRIGGER no_update_deleted_version_log
   BEFORE UPDATE ON version_log
   FOR EACH ROW EXECUTE FUNCTION synergy_block_update_on_deleted();

user_id cascades from profiles, and a cascading delete fires BEFORE DELETE triggers too: once a user has entities, deleting the account is refused unless the transaction sets app.allow_hard_delete. That is the overview's "erasure overrides", built in lesson 11.

Shell
pnpm test:integration

The throwaway database builds from every migration, and core's Data API test confirms the new tables gave anon and authenticated nothing.

3c — human

Rename person to human in ACTOR_KINDS, then:

Shell
pnpm db:generate --filter=@sidekick/synergy -- --name=actor_kind_human

0004_actor_kind_human.sql drops and re-adds both actor-kind CHECKs.

Step 4 — The per-user lock

What and why: an advisory lock is a number only our code gives meaning to; transactions asking for the same number take turns, and nothing else waits. Without it, two of one user's tabs can each pass a check the other is about to invalidate: tab A deletes note N after finding no live references, tab B links plan P to N after finding N live, and both commit (D202). With every Synergy write taking the user's lock first, B waits, re-reads and is refused. One lock per user is enough because RLS confines everything that can conflict to one user's data.

packages/platform/synergy/src/repository/lock.ts:

TypeScript
import { sql } from 'drizzle-orm';

import type { RlsTx } from '@sidekick/core/db';

// Every Synergy write calls this first, before it reads anything (D202).
// It takes the current user's lock, held until the transaction ends, so one
// user's Synergy writes run one at a time. Reads never wait for it.
async function lockCurrentUser(tx: RlsTx): Promise<void> {
   // The key is the user withRLS set, so the lock can't name another user.
   // Without withRLS, current_setting raises an error instead of locking nothing.
   await tx.execute(
      sql`select pg_advisory_xact_lock(hashtextextended(current_setting('app.current_user_id'), 0))`,
   );
}

export type SynergyMutation<Args extends unknown[], Result> = (
   tx: RlsTx,
   ...args: Args
) => Promise<Result>;

// Wraps a Synergy mutation so it takes the user's lock before anything else.
export function withUserLock<Args extends unknown[], Result>(
   mutation: SynergyMutation<Args, Result>,
): SynergyMutation<Args, Result> {
   return async function openUserLock(tx, ...args) {
      await lockCurrentUser(tx);
      return mutation(tx, ...args);
   };
}

_xact_ releases the lock at commit or rollback; a session-level lock would outlive the request on Supabase's transaction pooler. withUserLock arrived in step 5d, after the first two repository functions called the lock directly.

Step 5 — The repository

5a to 5d — registerEntity, saveEntityContent, getEntity, withUserLock

packages/platform/synergy/src/repository/errors.ts:

TypeScript
// Synergy's error codes. Keys only (D201): the client looks up the words.
// Lesson 4's routes turn each code into an HTTP status.
export type SynergyErrorCode = 'not_found' | 'stale_content';

export class SynergyError extends Error {
   constructor(readonly code: SynergyErrorCode) {
      super(code);
      this.name = 'SynergyError';
   }
}

packages/platform/synergy/src/db/utils.ts:

TypeScript
import { sql, type SQL } from 'drizzle-orm';
import type { AnyPgColumn } from 'drizzle-orm/pg-core';

export function increment(
   column: AnyPgColumn<{ dataType: 'number' }>,
): SQL<unknown> {
   return sql`${column} + 1`;
}

packages/platform/synergy/src/repository/entities.ts (as merged):

TypeScript
import { and, eq, isNull, sql, type InferSelectModel } from 'drizzle-orm';

import type { RlsTx } from '@sidekick/core/db';

import { entityIndex, versionLog } from '../db/schema';
import type { ACTOR_KINDS } from '../db/schema/columns';
import { increment } from '../db/utils';
import { SynergyError } from './errors';
import { withUserLock } from './lock';

type ActorKind = (typeof ACTOR_KINDS)[number];

// Who is writing. Mirrors the provenance CHECK: only the system has no id.
export interface SystemAsActor {
   kind: 'system';
   id?: never;
}
export interface NonSystemAsActor {
   kind: Exclude<ActorKind, 'system'>;
   id: string;
}

export type Actor = SystemAsActor | NonSystemAsActor;

// A row of entity_index, exactly as Drizzle returns it.
export interface EntityIndexRow extends InferSelectModel<typeof entityIndex> {}

// The user withRLS set. RLS's WITH CHECK refuses any other user_id anyway.
export const currentUserId = sql`current_setting('app.current_user_id')::uuid`;

export interface RegisterEntityPayload {
   id: string;
   provider: string;
   type: string;
   displayName: string;
   title?: string;
   actor: Actor;
}
// Creates an entity's index row. The provider calls it in the same transaction
// that inserts its own row.
export const registerEntity = withUserLock(
   async function registerEntityMutation(
      tx,
      payload: RegisterEntityPayload,
   ): Promise<EntityIndexRow> {
      const [entity] = await tx
         .insert(entityIndex)
         .values({
            id: payload.id,
            provider: payload.provider,
            type: payload.type,
            displayName: payload.displayName,
            title: payload.title,
            actorKind: payload.actor.kind,
            actorId: payload.actor.id,
            userId: currentUserId,
         })
         .returning();

      return entity;
   },
);

export interface SaveEntityContentPayload {
   id: string;
   // The content_seq the editor started from.
   baselineContentSeq: number;
   // The author flags a version; every other save is a revision.
   bump: 'revision' | 'version';
   changeNote?: string;
   displayName: string;
   title?: string;
   actor: Actor;
}

export const saveEntityContent = withUserLock(
   async function saveEntityContentMutation(
      tx,
      payload: SaveEntityContentPayload,
   ): Promise<EntityIndexRow> {
      const {
         id,
         baselineContentSeq,
         bump,
         changeNote,
         displayName,
         title,
         actor: { kind: actorKind, id: actorId },
      } = payload;
      const revision =
         bump === 'revision'
            ? increment(entityIndex.revision)
            : entityIndex.revision;
      const version =
         bump === 'version'
            ? increment(entityIndex.version)
            : entityIndex.version;
      const contentSeq = increment(entityIndex.contentSeq);

      const [updatedEntityRow] = await tx
         .update(entityIndex)
         .set({
            version,
            revision,
            contentSeq,
            displayName,
            title: title ?? null,
            editedAt: sql`now()`,
            updatedAt: sql`now()`,
            actorKind,
            actorId: actorId ?? null,
            reason: null,
         })
         .where(
            and(
               eq(entityIndex.id, id),
               eq(entityIndex.contentSeq, baselineContentSeq),
               isNull(entityIndex.deletedAt),
            ),
         )
         .returning();

      if (!updatedEntityRow) {
         // No match: either
         // 1. the baseline from which edit was initiated has shifted (updated by another tab or device),
         // 2. entity has been deleted, or
         // 3. it never existed in first place.
         // We hold the lock, so this answer can't change before we return it.
         const [live] = await tx
            .select({ id: entityIndex.id })
            .from(entityIndex)
            .where(and(eq(entityIndex.id, id), isNull(entityIndex.deletedAt)));

         throw new SynergyError(live ? 'stale_content' : 'not_found');
      }

      if (bump === 'version') {
         await tx.insert(versionLog).values({
            userId: currentUserId,
            entityId: id,
            version: updatedEntityRow.version,
            changeNote,
            actorKind,
            actorId: actorId ?? null,
         });
      }

      return updatedEntityRow;
   },
);

export interface GetEntityOptions {
   includeDeleted: boolean;
}

export async function getEntity(
   tx: RlsTx,
   id: string,
   options: GetEntityOptions,
): Promise<EntityIndexRow | undefined> {
   const [entity] = await tx
      .select()
      .from(entityIndex)
      .where(
         options.includeDeleted
            ? eq(entityIndex.id, id)
            : and(eq(entityIndex.id, id), isNull(entityIndex.deletedAt)),
      );
   return entity;
}
  • The stale check is one statement. UPDATE … SET content_seq = content_seq + 1 … WHERE content_seq = $base checks and moves the token together, so there is no window between them. Zero rows updated means refused; one follow-up read under the lock tells stale_content from not_found.
  • undefined versus null in Drizzle. In .set() and .values(), undefined means "leave this column out" and null means "write NULL". In an insert, leaving out gives the default; in an update, it keeps the old value. So saveEntityContent writes actorId ?? null: without it, a system save after a human one would keep the human's id beside actor_kind = 'system', and the provenance CHECK would refuse it.
  • includeDeleted is required, so every read states whether a tombstone counts (D66). Reads take no lock.
  • Names: the entity noun with a specific verb (registerEntity, saveEntityContent, later setEntityStatus, deleteEntity, restoreEntity), and layer suffixes for types (EntityIndexRow for a database row; the plain noun is kept for lesson 4's contract types).

5e — Interfaces for inferred types

EntityIndexRow, RlsTx (core) and GetEntityOptions are interfaces, so editors show their names instead of every field. typescript-eslint flags an empty interface that only extends something; the root eslint.config.js allows exactly that form:

JavaScript
{
   // Lets an inferred type be named as an interface (EntityIndexRow extends
   // InferSelectModel<...>), so editors show its name instead of every field.
   files: ['**/*.ts', '**/*.tsx'],
   rules: {
      '@typescript-eslint/no-empty-object-type': [
         'error',
         { allowInterfaces: 'with-single-extends' },
      ],
   },
},

5f — sidekick/require-user-lock

packages/eslint-plugin-sidekick/src/rules/require-user-lock.ts reports any call to a method named insert, update or delete that isn't inside the function passed to an imported withUserLock(...). It checks each write call, not each export, so a helper that writes is caught too. It can't see types (map.delete would be flagged) or raw SQL through tx.execute. Register it in src/index.ts and turn it on in the root config:

JavaScript
{
   // Every Synergy write takes the per-user lock first (D202): each insert,
   // update or delete in the repository must sit inside withUserLock(...).
   files: ['packages/platform/synergy/src/repository/**/*.ts'],
   plugins: { sidekick: sidekickPlugin },
   rules: {
      'sidekick/require-user-lock': 'error',
   },
},

Its tests (require-user-lock.test.ts) pass with the plugin's others. Planting an unwrapped tx.delete(entityIndex) in the repository fails pnpm lint with the rule's message.

Gotcha — restart the editor's ESLint server after a plugin change. The root config loads the plugin from its compiled dist/. Cursor's ESLint server had loaded the old copy; when the config named a rule that copy lacked, it stopped linting every file the rule covers and showed no errors at all. pnpm lint builds the plugin first and was never stale. Rebuild, then run ESLint: Restart ESLint Server (Commands and environment).

Step 6 — Prove it

6a — Let Synergy's tests run repository code

packages/platform/synergy/vitest.config.ts aliases server-only to tests/stubs/server-only.ts (an empty module, copied from core), and the config joins tsconfig.json's include. Core's database client starts with import 'server-only', which throws outside a Next.js server.

6b — Strict restore

Shell
pnpm db:generate --filter=@sidekick/synergy -- --custom --name=strict_restore

0005_strict_restore.sql:

SQL
-- Restores are strict (lesson 3): an update to a soft-deleted row is allowed only
-- when it restores the row and changes nothing but deleted_at and the bookkeeping
-- columns. A restore that also edits is two statements in one transaction:
-- restore, then a normal save (which runs the content_seq check).
-- Synergy's own function: only Synergy's tables bind it, so replacing it here
-- changes exactly those tables.
CREATE OR REPLACE FUNCTION synergy_block_update_on_deleted()
RETURNS trigger LANGUAGE plpgsql AS $$
BEGIN
    IF OLD.deleted_at IS NULL THEN
        RETURN NEW;
    END IF;

    IF NEW.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;

    -- The row as JSON, minus the columns a restore may change, must not differ
    IF to_jsonb(NEW) - ARRAY['deleted_at', 'updated_at', 'actor_kind', 'actor_id', 'reason']
        IS DISTINCT FROM
        to_jsonb(OLD) - ARRAY['deleted_at', 'updated_at', 'actor_kind', 'actor_id', 'reason'] THEN
        RAISE EXCEPTION
            'A restore in % (id: %) may change only deleted_at and the provenance columns.',
            TG_TABLE_NAME, OLD.id;
    END IF;

    RETURN NEW;
END;
$$;

Listing the columns a restore may change, rather than the ones it may not, protects every column added later. restore-aware-trigger.test.ts gains "refuses a restore that also changes other columns", on its own probe row: the first version reused row 2, which the existing restore test had already brought back, so it passed an ordinary update and failed. Each test owns the rows it changes.

6c — The repository tests

packages/platform/synergy/tests/integration/entities.test.ts runs as app_runtime through withRLS, the path a provider takes, and proves:

  1. register, then save twice: revision 2, content_seq 2, version 1;
  2. a second save from the same stale content_seq gets stale_content;
  3. a version save logs a version_log row with its note and leaves revision alone;
  4. another user can't read the entity, and their save gets not_found;
  5. a hard delete is refused;
  6. a tombstone is visible only with includeDeleted: true;
  7. a deleted row can be restored, but not edited, nor edited while restoring;
  8. saving content to a deleted entity gets not_found.

Each test registers its own entity. The delete and restore tests write rows directly, since the repository gets those functions in lesson 11. Drizzle wraps a database error as Failed query: … with the trigger's message on error.cause, so those assertions check the cause.

Shell
pnpm test:integration

Synergy 13, core 17.

6d — Break each guard

One at a time, rerunning pnpm test:integration --filter=@sidekick/synergy after each:

  1. comment out eq(entityIndex.contentSeq, baselineContentSeq) in saveEntityContent: "refuses a save from a stale content_seq" fails;
  2. in a scratch custom migration (--name=break_guards), ALTER POLICY entity_index_own_rows ON entity_index USING (true) WITH CHECK (true);: "hides another user's entity" fails;
  3. DROP TRIGGER no_hard_delete_entity_index ON entity_index;: "refuses a hard delete" fails;
  4. DROP TRIGGER no_update_deleted_entity_index ON entity_index;: "can restore a deleted row but not otherwise update it" fails.

Remove the scratch migration's three parts and the suite is back to 13/13.

Step 7 — Delete models

  • profiles.ts and user-feature-entitlements.ts in core each name their model in a comment (hard delete), as Synergy's schema files do.
  • Core 0010_block_update_on_deleted_message.sql replaces block_update_on_deleted with the same body and a corrected message: "This table does not allow restores." The old message said "Restore it first", which the function forbids. Nothing binds it today, and shared SQL functions are public API, so the behaviour is unchanged.
Shell
pnpm db:generate --filter=@sidekick/core -- --custom --name=block_update_on_deleted_message

(The first attempt ran with --filter=@sidekick/synergy and produced a Synergy 0006; its three parts were removed before regenerating in core.)

Exit check: pnpm prettier:check, pnpm turbo run lint typecheck build test --force, pnpm test:integration (Synergy 13, core 17), CI green on sidekick#58, and pnpm db:migrate on dev (core 0010, Synergy 0001 to 0005).

Arkive

Index

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

Tags (145)

View

246 entries.

As ofTitleKind
2026-11-14Delete Modelssystem-design database soft-delete rlsspec
2026-11-14Synergy Lesson 3 Walkthrough: Entity Index and Version LogReadingbuild walkthrough phase-3 synergy database rls postgresqlguidance
2026-11-14Public Site: System Designsystem-design site one-repospec
2026-11-14Decisions Logdecisionlog
2026-11-14Synergy Lesson 11: Delete and Restoreplan phase-3 task synergy databaseplan
2026-11-14Synergy Lesson 3: Entity Index and Version Logplan phase-3 task synergy database rls postgresqlplan
2026-11-14Synergy Lesson 4: Sandbox Providers and the First Routesplan phase-3 task synergy feature-system apiplan
2026-11-14Tombstone Visibilitysystem-design synergy rlsspec
2026-11-14Synergy: System Designsystem-design synergyspec
2026-11-14Agent Guideai-coding agent gateway agent-guidance rls supabase drizzle api react prettierguidance
2026-11-14Backlogged / Unplannedplan living-plan backlog feature-system taxila zinsser alter-ego parrot rls supabaseplan
2026-11-14Commands and Environmentai-coding command environment agent-guidance prettier pnpm cliguidance
2026-11-14Database and Securityai-coding database security agent-guidance api-guard rls drizzle api react repository-patternguidance
2026-11-14Migrationsystem-design database drizzle pnpmspec
2026-11-14Non-Syncable Tablessystem-design database offlinespec
2026-11-14Row-Level Securitysystem-design security rls drizzle offlinespec
2026-11-14Soft-Delete Supportsystem-design database soft-deletespec
2026-11-14Syncable Tablessystem-design database offline soft-deletespec
2026-11-14Synergyplan living-plan phase-3 synergy rls postgresql api graphplan
2026-11-14tsupsystem-design code-quality-check eslintspec
2026-11-11Synergy Lesson 0 Walkthrough: One Repobuild walkthrough phase-3 monorepo one-repo git cloudflareguidance
2026-11-11Synergy Lesson 1 Walkthrough: Layer Folders and Lintbuild walkthrough phase-3 monorepo eslintguidance
2026-11-11Synergy Lesson 2 Walkthrough: Per-Package Migrationsbuild walkthrough phase-3 database drizzle testingguidance
2026-11-11Hosting and Domainssystem-design deployment vercel cloudflare domainspec
2026-11-11Synergy Lesson 0: One Repoplan phase-3 task monorepo one-repo git cloudflareplan
2026-11-11Synergy Checkpoint 3a: Prove the Store, Plan 3bplan phase-3 checkpoint guidanceplan
2026-11-11Synergy Lesson 1: Layer Folders and Lintplan phase-3 task monorepo eslintplan
2026-11-11Synergy Lesson 2: Per-Package Migrationsplan phase-3 task database drizzle testingplan
2026-11-11Lexicon Highlightingsystem-design synergy taxilaspec
2026-11-11Testing Layerssystem-design testing vitest playwrightspec
2026-11-11Synergy - Feature Specificationsprd spec synergyprd
2026-11-11Gatekeeper Walkthroughbuild walkthrough phase-2 api-guard feature-system rls authenticationguidance
2026-11-11Best Practice Rulessystem-design code-quality-check eslintspec
2026-11-11Centralized Copysystem-design contentspec
2026-11-11Dependency Flowsystem-design monorepo clispec
2026-11-11ESLint Plugin Boundariessystem-design code-quality-check eslintspec
2026-11-11Front Door: Row-Level Security Walkthroughplan phase-1.1 rls database supabase drizzle postgresql api typescriptguidance
2026-11-11Installation at Workspace Levelsystem-design code-quality-check typescript pnpmspec
2026-11-11Prettier at Repo Levelsystem-design code-quality-check prettierspec
2026-11-11Shared tsconfigsystem-design code-quality-check typescriptspec
2026-11-09Notesai-coding-harness agent-guidance nextjs eslint pnpm cssguidance
2026-11-08Violations and Exceptionssystem-design synergyspec
2026-11-08Front Door: Hardening Walkthroughbuild walkthrough pre-2 rls supabase drizzle postgresql testing eslintguidance
2026-11-08Alter Egoplan living-plan phase-6 taxila zinsser alter-ego war-room factory core-driveplan
2026-11-08API Versioningsystem-design apispec
2026-11-08Approve Buildssystem-design monorepo nextjs pnpmspec
2026-11-08Best Practicesai-coding-harness agent-guidance drizzle api react pnpm css offline embedding soft-deleteguidance
2026-11-08dotenv CLIsystem-design config clispec
2026-11-08Pocketplan living-plan phase-8 taxila alter-ego nextjs vercel pwa mobileplan
2026-11-08Server Actionssystem-design nextjs rls api authenticationspec
2026-11-08Taxilaplan living-plan phase-4 taxila api-guard rls drizzle api authenticationplan
2026-11-08War Roomplan living-plan phase-7 war-room factory core-drive rls api offlineplan
2026-11-08With API Guardsystem-design security api-guard rls api authentication typescriptspec
2026-11-08Zinsserplan living-plan phase-5 taxila zinsser api mantine ai embeddingplan
2026-11-06Design Tokenssystem-design css design-systemspec
2026-11-06Native UIsystem-design css design-system mantinespec
2026-11-06Internationalizationsystem-design content i18nspec
2026-11-06CSS Modulessystem-design css eslint stylelintspec
2026-11-06Runtime Validationssystem-design typescriptspec
2026-11-06Technology Stacksystem-design supabase drizzle postgresql nextjs typescript turborepo pnpm vercelspec
2026-11-06Architectural OverviewSidekick's architectural overview — an API-first platform tuned for solo developerssidekick project technical architecturedocument
2026-11-05One Repoprd feature synergy one-repoprd
2026-11-05Overview of Synergyprd feature synergyprd
2026-11-01Synergy Lesson 10: Save Call, Anchorsplan phase-3 task synergy apiplan
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 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-01Content Sequencesystem-design synergyspec
2026-11-01Anchors in Contentsystem-design synergy anchorsspec
2026-11-01Capture External Contentsystem-design synergy taxilaspec
2026-11-01Mentions and Link Lifetimesystem-design synergyspec
2026-11-01Synergy Store Choicessystem-design synergy postgresqlspec
2026-11-01Relationshipsprd feature synergyprd
2026-11-01Scope and Phasesprd feature synergyprd
2026-11-01Stay on ESLint, Revisit Oxlintsystem-design eslint typescriptspec
2026-11-01App Runtime for Drizzlesystem-design database drizzle postgresqlspec
2026-11-01Corepacksystem-design monorepo pnpmspec
2026-11-01Edge Runtimesystem-design security supabasespec
2026-11-01Env Vars in Turbo JSONsystem-design monorepo turborepo vercelspec
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-01Living Implementation Planplan living-plan taxila zinsser alter-ego war-room factory parrot core-driveplan
2026-11-01Module Resolution: Bundlersystem-design code-quality-check typescriptspec
2026-11-01Profile Creation Postgres Triggersystem-design security postgresql api authenticationspec
2026-11-01Public API Onlysystem-design api rls authentication clispec
2026-11-01Turboreposystem-design monorepo turborepo pnpmspec
2026-10-31Build mind palaceprd value-proposition specificationprd
2026-10-31Exponential Evolutionprd value-proposition specificationprd
2026-10-31Remember everythingprd value-proposition specificationprd
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-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 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-24Gatekeeperplan living-plan phase-2 api-guard rls api authentication feature-systemplan
2026-10-24Terminalplan living-plan phase-9 taxila api-guard rls api authentication cliplan
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-23Error Handlingsystem-design observability api-guard rls supabase api nextjs react aispec
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-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 Egosystem-design prd feature taxila alter-ego factory aiprd
2026-09-19API Firstsystem-design api clispec
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-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-19Model Router Contractsystem-design ai model-routing provider-agnostic embeddingspec
2026-09-19Never Mutate Outside API Layersystem-design apispec
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-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-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-19System Overview Diagramsystem-designspec
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-19Watchtowerplan living-plan phase-10 api-guard rls api authentication nextjs offlineplan
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