Architecture Handover

Status: Canonical Architecture Document
Audience: Solo developer initially, future small engineering team
Last Updated: May 2026

1. Executive Summary

This application is a modular, API-first productivity platform built as:

  • A web application (Next.js App Router)
  • A Progressive Web App (PWA)
  • A future native iOS shell (Capacitor)
  • A programmable API platform for agents, automations, workflows, and CLI tooling

The architecture intentionally prioritizes:

  1. Security through enforceable structure
  2. Long-term maintainability for a solo developer
  3. Incremental scalability
  4. Future offline-readiness
  5. API parity across browser, CLI, and agents

The system is NOT designed as a hyperscale enterprise platform from day one. Instead, it is designed to evolve safely without major rearchitecture.

2. Architectural Philosophy

All mutations flow through /api/*.

This guarantees:

  • Consistent business logic
  • Agent compatibility
  • CLI compatibility
  • Future sync compatibility
  • Easier observability
  • Centralized authorization

Even browser interactions ultimately use the same API layer.

The system avoids “remember to do X” security patterns.

Instead:

  • Route security is centralized
  • RLS context is centralized
  • Feature entitlement checks are centralized
  • API scope checks are centralized

This reduces long-term drift and accidental vulnerabilities.

Features are isolated into packages.

However, the MVP intentionally uses:

  • Build-time feature registration
  • Shared deployment artifact
  • Shared runtime

This avoids premature complexity.

The architecture preserves the ability to evolve into:

  • runtime plugin loading
  • microservices
  • independent deployments
  • offline sync engines

without large-scale rewrites.

3. System Overview

┌─────────────────────────────────────────┐
│ Clients                                 │
│                                         │
│ Browser │ PWA │ CLI │ Agents │ iOS     │
└─────────────────────────────────────────┘


┌─────────────────────────────────────────┐
│ API Layer (/api/*)                     │
│                                         │
│ withApiGuard()                         │
│ ├── Auth                               │
│ ├── Feature Entitlement                │
│ ├── RLS Context                        │
│ ├── Scope Validation                   │
│ └── Handler Execution                  │
└─────────────────────────────────────────┘


┌─────────────────────────────────────────┐
│ PostgreSQL (Supabase)                  │
│                                         │
│ RLS Policies                           │
│ Feature Tables                         │
│ Vector Search                          │
└─────────────────────────────────────────┘

4. Monorepo Structure

apps/
  web/
  cli/

packages/
  core/
  ui/
  features-registry/
  feature-notes/
  feature-writing/
  feature-bookmarks/
  feature-recipes/
  feature-budget/
  feature-ai-chat/

5. Dependency Rules

packages/* MUST NEVER import from apps/*.

This is a hard architectural constraint.

Violating this introduces:

  • circular dependencies
  • invalid build graphs
  • hidden coupling
  • future deployment problems
apps/*

packages/features/*

packages/core

ALL_FEATURES lives in:

packages/features-registry

This package owns:

  • feature manifests
  • feature metadata
  • feature registration

core MUST remain feature-agnostic.

6. Technology Stack

LayerChoice
FrontendNext.js 16 App Router
LanguageTypeScript Strict
DBSupabase PostgreSQL
ORMDrizzle ORM
StylingMantine
EditorTiptap
AI SDKVercel AI SDK
LLMAnthropic Claude
EmbeddingsOpenAI text-embedding-3-small
MonorepoTurborepo + pnpm
HostingVercel
Native ShellCapacitor

7. Security Architecture

7.1 Canonical API Security Model

ALL /api/* routes MUST use:

withApiGuard()

Direct route handlers are prohibited.

7.2 Security Flow

Every request passes through:

  1. Authentication
  2. Feature entitlement validation
  3. RLS context setup
  4. API scope validation
  5. Business logic execution

This flow is mandatory.

7.3 Why This Matters

Without centralization:

  • routes drift over time
  • developers forget checks
  • RLS gets bypassed
  • feature gating becomes inconsistent

The architecture intentionally makes secure behavior the default.

8. Authentication

Browser/PWA/iOS

Supabase cookie session.

CLI / Agents

Bearer API keys.

API keys support:

  • SHA-256 hashing
  • scopes
  • expiry
  • revocation
  • last-used tracking

Example scopes:

notes:read
notes:write
recipes:read
chat:write
export const apiKeys = pgTable('api_keys', {
  id: uuid('id').primaryKey(),

  userId: uuid('user_id')
    .notNull()
    .references(() => profiles.id, {
      onDelete: 'cascade'
    }),

  keyHash: text('key_hash').notNull(),

  label: text('label'),

  scopes: text('scopes').array().default(sql`'{}'`),

  expiresAt: timestamp('expires_at'),

  revokedAt: timestamp('revoked_at'),

  lastUsedAt: timestamp('last_used_at'),
})

9. Row-Level Security (RLS)

Drizzle connects as app_runtime, a non-superuser PostgreSQL role. RLS is enforced at the database level for all Drizzle queries.

profiles table (no deletedAt — not a syncable entity):

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

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

Phase 3+ syncable feature tables (notes, bookmarks, recipes, etc. — all have deletedAt):

ALTER TABLE table_name ENABLE ROW LEVEL SECURITY;
ALTER TABLE table_name FORCE ROW LEVEL SECURITY;

CREATE POLICY "users_own_rows"
ON table_name
FOR ALL
USING (
  user_id::text = current_setting('app.current_user_id', true)
  AND deleted_at IS NULL
)
WITH CHECK (
  user_id::text = current_setting('app.current_user_id', true)
);

The USING clause combines two constraints: user isolation and soft-delete filtering. Both are enforced at the database level — not by application convention.

WITH CHECK intentionally omits deleted_at IS NULL — a restore operation (setting deleted_at = NULL) is a valid UPDATE that must be allowed through.

FORCE ROW LEVEL SECURITY ensures the policy applies even to the table owner role. Belt and suspenders.

The application MUST NEVER manually inject RLS context inline.

Use:

withRLS(userId)

only.

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)
  })
}

The transaction wrapper is mandatory. set_config with is_local = true only resets at the end of the current transaction. Without the wrapper, the setting has no transaction to bind to and persists on the pooled connection — leaking the user ID to the next request.

This removes:

  • duplicated SQL
  • injection risk
  • inconsistent setup
  • user ID leakage across pooled connections

Two shared trigger functions are defined once in migration 0003_soft_delete_trigger_fns.sql and reused across all syncable tables.

-- Blocks hard deletes unless explicitly opted in
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;
$$;

-- Blocks updates on already soft-deleted rows
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;
$$;

Each Phase 3+ feature migration binds these functions to its table:

CREATE TRIGGER no_hard_delete_[table]
  BEFORE DELETE ON [table]
  FOR EACH ROW EXECUTE FUNCTION enforce_soft_delete();

CREATE TRIGGER no_update_deleted_[table]
  BEFORE UPDATE ON [table]
  FOR EACH ROW EXECUTE FUNCTION block_update_on_deleted();

Enforcement summary:

ConstraintMechanismEnforced at
User sees only their own rowsRLS USING clauseDatabase
Soft-deleted rows invisible to usersRLS USING clauseDatabase
Hard deletes blockedBEFORE DELETE triggerDatabase
Updates on deleted rows blockedBEFORE UPDATE triggerDatabase
SELECT filtering (belt)where(isNull(deletedAt)) in reposApplication

Triggers fire for all roles including service role and superuser. RLS is enforced because Drizzle connects as app_runtime (non-superuser). createAdminClient() bypasses RLS but not triggers.

10. API Guard

withApiGuard() centralizes:

  • auth
  • feature checks
  • RLS
  • scope validation
export function withApiGuard(handler, opts = {}) {
  return async (req) => {
    const auth = await resolveApiCaller(req)

    if (!auth?.userId) {
      return Response.json({ error: 'Unauthorized' }, { status: 401 })
    }

    if (opts.feature) {
      const features = await getEnabledFeatures(auth.userId)

      if (!features.some(f => f.slug === opts.feature)) {
        return Response.json({ error: 'Feature disabled' }, { status: 403 })
      }
    }

    return withRLS(auth.userId, async (tx) => {
      if (opts.requireScope && auth.isApiKey) {
        if (!auth.scopes?.includes(opts.requireScope)) {
          return Response.json({ error: 'Forbidden' }, { status: 403 })
        }
      }

      return handler({
        tx,
        userId: auth.userId,
        req,
      })
    })
  }
}

11. API Route Pattern

export const GET = withApiGuard(handler, options)

This is mandatory.

export const GET = withApiGuard(
  async ({ tx }) => {
    const rows = await tx.select().from(notes)

    return Response.json({ data: rows })
  },
  {
    feature: 'notes',
    requireScope: 'notes:read'
  }
)

12. Repository Architecture

UI → Repository → API → Database

This abstraction is intentionally future-oriented.

Today:

Repository → API

Future:

Repository → Local DB → Sync Engine → API

The UI layer must not care.

Server Actions ARE allowed.

However:

  • they must use repositories
  • they must not bypass APIs
  • they must not bypass authorization

This preserves API-first guarantees.

13. Offline-Ready Design

Offline mode is NOT an MVP feature.

However, the architecture intentionally avoids blocking future implementation.

Client-generated IDs

Clients generate UUIDs.

This prevents sync collision problems later.

Idempotent APIs

Repeated requests with the same ID must produce the same result.

This is critical for sync reliability.

Repository Layer Mandatory

The repository layer MUST NOT be bypassed.

This is the primary abstraction boundary enabling future sync support.

updatedAt Is Source of Truth

Every syncable entity includes:

createdAt
updatedAt
deletedAt

Future conflict resolution depends on updatedAt.

deletedAt enables soft deletes. This is mandatory from day one — hard deletes cannot propagate to offline clients and cannot be undone after sync data exists. Never use hard deletes on syncable entities.

Soft Deletes Are Mandatory

All syncable entities MUST use soft deletes.

deletedAt: timestamp('deleted_at')  // null = active, timestamp = deleted

Hard deletes are prohibited on syncable tables because:

  • Offline clients cannot receive a deletion event for a row that no longer exists
  • Sync conflict resolution requires tombstone records
  • Data recovery becomes impossible after a hard delete

All queries against syncable tables MUST filter deleted records:

where(isNull(table.deletedAt))

14. Embedding Pipeline

Embedding writes must be:

  • asynchronous
  • atomic
  • retryable
  • observable

Every content table that participates in the embedding pipeline MUST include an embeddingStatus field:

embeddingStatus: text('embedding_status')
  .notNull()
  .default('pending')  // 'pending' | 'complete' | 'failed'

This field is the source of truth for embedding state. It enables:

  • querying for un-embedded or failed content
  • manual or automated retry of failed jobs
  • visibility into pipeline health without log-scraping
  • safe re-embedding after model upgrades

Status transitions:

pending → complete   (successful embedding write)
pending → failed     (all retries exhausted)
failed  → pending    (manual or automated retry trigger)

Any content with embeddingStatus = 'failed' MUST be logged and retryable. Silent failures are not acceptable.

Embeddings must be written in a transaction.

Never:

delete → insert (outside transaction)

This can temporarily remove embeddings.

Embedding jobs:

  • retry twice
  • exponential backoff
  • log all failures
  • set embeddingStatus = 'failed' after retries exhausted

At minimum:

  • structured logs
  • failed embedding logs
  • latency visibility

MVP does NOT require full observability infrastructure.

15. Feature System

Features are:

  • build-time registered
  • package isolated
  • entitlement controlled

This is intentional.

Inactive features are still included in the build.

This is acceptable for MVP.

The architecture can later evolve toward:

  • runtime plugin loading
  • independent deployments
  • feature microservices

without major rewrites.

16. Database Migrations

Every feature package owns:

schema.ts
drizzle.config.ts
migrations/

There is NO global drizzle config.

Root command:

pnpm db:migrate

This script:

  1. discovers packages
  2. executes migrations in deterministic order
  3. fails fast on errors

This prevents:

  • schema drift
  • inconsistent environments
  • hidden migration dependencies

17. Background Jobs

Use lightweight async background execution:

  • waitUntil()
  • Vercel background execution
  • retry wrappers

Can evolve toward:

  • Inngest
  • queues
  • cron workflows
  • distributed workers

without changing API contracts.

18. Observability

  • request logging
  • failed job logging
  • API latency logging
  • auth failure logging

Inside:

withApiGuard()

This provides centralized visibility.

19. Developer Rules

Rule 1

All API routes MUST use:

withApiGuard()

Rule 2

Never manually set RLS context.

Use:

withRLS()

only.

Rule 3

Never mutate data outside the API layer.

Rule 4

Never import from apps/* inside packages/*.

Rule 5

Repository layer must not be bypassed.

Rule 6

All syncable APIs should be idempotent.

Rule 7

Never hard-delete syncable entities.

Use soft deletes:

deletedAt: timestamp('deleted_at')

All queries against syncable tables must filter where(isNull(table.deletedAt)).

Rule 8

All content tables participating in the embedding pipeline MUST include an embeddingStatus field.

Set it to 'failed' after retries are exhausted. Never silently drop failed embedding jobs.

Rule 9

Never use createAdminClient().from(...).delete() to hard-delete rows from syncable tables.

The BEFORE DELETE trigger fires for all roles including service role. The delete will be rejected regardless of the client used.

Hard-deletes that must bypass the trigger (e.g. GDPR erasure) require a dedicated Drizzle transaction:

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

SET LOCAL scopes the flag to the transaction — it resets on commit or rollback. This is the only legitimate hard-delete pathway.

20. Operational Details Preserved From Original Handover

The original handover contained important operational environment variables that must remain part of the canonical architecture.

Supabase

NEXT_PUBLIC_SUPABASE_URL=
NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY=    # renamed from NEXT_PUBLIC_SUPABASE_ANON_KEY in 2025
SUPABASE_SECRET_KEY=                      # renamed from SUPABASE_SERVICE_ROLE_KEY in 2025
DATABASE_URL=          # port 6543, pooler — runtime queries
DATABASE_DIRECT_URL=   # port 5432, direct — Drizzle migrations only (not needed in Vercel)

AI Providers

ANTHROPIC_API_KEY=
OPENAI_API_KEY=

Billing

STRIPE_SECRET_KEY=
STRIPE_WEBHOOK_SECRET=
NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY=

App

NEXT_PUBLIC_APP_URL=

The architecture intentionally separates four clients. Never mix them.

ClientFileKeyUsed In
createBrowserClient()browser.tspublishable keyClient Components ('use client')
createServerClient()server.tspublishable key + cookiesServer Components, Route Handlers (Node.js runtime)
createProxyClient(req, res)proxy.tspublishable key + request cookiesproxy.ts only (Edge runtime)
createAdminClient()admin.tssecret key (bypasses RLS)Server-only, trusted operations

Import via subpath: @sidekick/core/supabase/browser, @sidekick/core/supabase/server, etc.

Why the proxy client is separate

proxy.ts runs in the Edge runtime, which cannot import next/headers. createServerClient() uses next/headers internally — calling it from proxy.ts would crash at runtime. createProxyClient(request, response) reads cookies from the incoming Request and outgoing Response directly, with no next/headers dependency. It must be used exclusively in proxy.ts and nowhere else.

Next.js 16 renamed the middleware convention:

  • File: middleware.tsproxy.ts
  • Export: export function middlewareexport function proxy
  • The config export is unchanged

proxy.ts is responsible for:

  • session refresh (via createProxyClient)
  • redirecting unauthenticated browser users
  • excluding API routes from redirect behavior

proxy.ts MUST NOT contain business authorization logic.

Authorization belongs in:

withApiGuard()

The original handover included important Mantine setup requirements.

Required Imports

@import '@mantine/core/styles.css';
@import '@mantine/notifications/styles.css';
@import '@mantine/tiptap/styles.css';

Required Providers

<MantineProvider>
<Notifications />

PostCSS Plugins

postcss-preset-mantine
postcss-simple-vars

These are required for Mantine CSS variable resolution.

Hydration Fix

Add suppressHydrationWarning to the <html> element. ColorSchemeScript injects a data-mantine-color-scheme attribute via a script tag before React hydrates — without suppressHydrationWarning, React will emit a hydration warning because the attribute wasn’t present during server render.

Set defaultColorScheme="auto" on BOTH ColorSchemeScript AND MantineProvider. A mismatch between the two causes hydration errors.

<html suppressHydrationWarning>
  <head>
    <ColorSchemeScript defaultColorScheme="auto" />
  </head>
  <body>
    <MantineProvider defaultColorScheme="auto">
      {children}
    </MantineProvider>
  </body>
</html>

All styling uses CSS modules. No exceptions.

What is banned

Pure Mantine style props that set visual styles inline:

// BANNED — style props
<Box h={100} px="md" fw={700} c="red" mt={8} />

What is allowed

Mantine behavioral props that configure component behavior, not visual style:

// ALLOWED — behavioral props
<AppShell navbar={{ width: 240, breakpoint: 'sm' }} withBorder shadow="sm" />

Enforcement

packages/eslint-plugin-sidekick contains the no-mantine-style-props rule. It is compiled with tsup (not raw tsc) because ESLint plugins must run as CommonJS in Node.js and cannot load .ts files directly.

The rule is registered in the root eslint.config.js and fails lint immediately on any violation.

All user-visible strings live in packages/copy. Never hardcode strings directly in source files.

import { copy } from '@sidekick/copy'

// Use
<Button>{copy.auth.signIn}</Button>

Why: Consistent copy across apps/web and apps/cli. Copy changes happen in one place. TypeScript as const makes copy type-safe and autocomplete-friendly.

useNavigation hook

Always use useNavigation() instead of calling router.push() alone. The hook calls router.push() followed by router.refresh() together. Forgetting router.refresh() after auth actions leaves the UI in a stale server-rendered state.

const { navigate } = useNavigation()
navigate('/dashboard')  // push + refresh

force-dynamic on Supabase-touching route groups

Add export const dynamic = 'force-dynamic' to the layout of every route group that touches Supabase (e.g. (app)/layout.tsx, (auth)/layout.tsx). Without it, Next.js may attempt to statically pre-render these layouts at build time, which fails because Supabase cookie reads are request-time operations.

User profiles are created via a Postgres trigger on auth.users, not an API route.

CREATE FUNCTION public.create_profile_for_new_user()
RETURNS trigger AS $$
BEGIN
  INSERT INTO public.profiles (id, email, created_at)
  VALUES (NEW.id, NEW.email, NOW());
  RETURN NEW;
END;
$$ LANGUAGE plpgsql SECURITY DEFINER;

CREATE TRIGGER on_auth_user_created
  AFTER INSERT ON auth.users
  FOR EACH ROW EXECUTE FUNCTION public.create_profile_for_new_user();

Critical: the function must use public.profiles (fully qualified) because triggers run in the auth schema context. An unqualified profiles reference would fail to resolve.

Why a trigger instead of an API route:

  • Works for all auth providers (email, OAuth, magic link) without per-provider app-level code
  • Cannot fail silently after auth succeeds — the profile creation is part of the same database transaction
  • No race conditions between auth completion and API calls

GraphQL + Relay — Deferred to Post-MVP

REST API for MVP. GraphQL evaluation is deferred.

Why not now:

  • Cognitive load during learning phase
  • Relay + App Router integration is unresolved upstream
  • withApiGuard maps cleanly to REST; adapting it to a GraphQL resolver layer requires rethinking

When to revisit: After MVP ships, when data-fetching complexity justifies it, or when Relay/App Router integration matures.

How to add: Could replace or augment REST without full architectural rework. withApiGuard would need a GraphQL resolver adapter.

API Versioning (/api/v1/) — Deferred to Post-MVP

Current API uses /api/ with no version prefix.

Why not now: Adds URL complexity for no current benefit. MVP has one client. Breaking changes can be coordinated directly.

When to add: When multiple external clients need migration time, or when breaking changes become frequent.

How to add: Route group at /api/v1/ in Next.js. No architectural rework needed — just move route handlers into the versioned group.

The original architecture contained several important editor requirements.

Requirements

  • JSON storage format
  • Markdown export support
  • Rich text toolbar
  • Mobile-friendly editing
  • Semantic chunk generation for embeddings

Important Constraint

Embedding generation should operate on semantic markdown output rather than raw text extraction whenever possible.

The original handover included important AI pipeline details.

Requirements

  • pgvector enabled
  • HNSW index
  • semantic chunking
  • overlap chunk strategy
  • async embedding generation
  • retrieval-augmented generation
  • streaming AI responses

Required Database Function

match_content()

This function remains part of the canonical design.

The CLI remains a first-class architectural citizen.

Responsibilities

  • authenticated API access
  • streaming chat support
  • automation support
  • local scripting support
  • future agent interoperability

Canonical Principle

The CLI must use the same public API surface as external agents.

The original handover included important PWA constraints.

Requirements

  • installable web app
  • manifest.json
  • service worker
  • offline asset caching
  • mobile-compatible shell

Canonical Tooling

Serwist

The MVP native strategy remains:

Capacitor + hosted Next.js application

The architecture intentionally delays:

  • embedded offline DB
  • native sync engine
  • fully local-first execution

until post-MVP.

The phased rollout strategy from the original handover remains valid.

Canonical Order

  1. Monorepo foundation
  2. Auth + shell
  3. Notes feature
  4. Writing feature
  5. Content features
  6. AI layer
  7. Billing
  8. Bots / workflows
  9. Native shell

This phased sequence intentionally reduces architectural risk.

Constraint 1

Feature manifests remain the canonical feature contract.

Constraint 2

All features must enforce entitlement checks at API boundaries.

Constraint 3

Background embedding generation must never block user writes.

After all retries are exhausted, embeddingStatus must be set to 'failed'. Failed jobs must remain queryable and retryable.

Constraint 3a

All syncable entities must include deletedAt for soft delete support. Hard deletes are prohibited on syncable tables.

Constraint 4

Server Components are preferred for data-fetching.

Constraint 5

Client Components should only exist where interactivity is required.

Constraint 6

Drizzle must never execute in browser/client components.

Constraint 7

Repository abstractions are mandatory for all mutations.

21. Repository Visibility

The GitHub repository is public.

This is intentional. The project is built in the open as a learning exercise and portfolio. Friends and collaborators can view progress without requiring explicit invitations.

Security in this architecture comes from correct implementation, not obscurity:

  • RLS policies enforce data isolation at the database level regardless of who reads the source code
  • withApiGuard() centralizes authorization — knowing the code exists doesn’t bypass it
  • API keys are hashed (SHA-256) before storage — the schema being public is irrelevant
  • .env.local is gitignored — real secrets never enter the repository

Making the architecture and implementation decisions public is consistent with standard open-source practice. The actual security surface is the running application, not the source code.

The following must never be committed to the repository under any circumstances:

  • .env.local or any file containing real environment variable values
  • Supabase service role keys
  • API keys (Anthropic, OpenAI, Stripe)
  • Database connection strings with credentials
  • Any token, password, or private key

The .gitignore blocks .env* files (with the exception of .env.example). This is a technical safeguard, not a substitute for vigilance. Always verify git status before committing.

If a secret is ever accidentally committed:

  1. Immediately rotate the exposed key/token in the relevant service dashboard
  2. Remove the secret from git history using git filter-repo or GitHub’s secret scanning remediation tools
  3. Force-push the cleaned history

Rotation is mandatory — removing from git history is not sufficient on its own because the secret may already have been cloned or cached.

22. Final Architectural Position

This architecture intentionally optimizes for:

  • maintainability
  • correctness
  • solo-developer velocity
  • future extensibility

while explicitly avoiding:

  • premature microservices
  • premature offline complexity
  • runtime plugin overengineering
  • unnecessary infrastructure

The system is designed to evolve safely over time without foundational rewrites.