Boring Docs

Build Features — The Data-First Protocol

The complete engineering manual for humans and autonomous agents to build, wire, and ship any feature across SQLite, MongoDB, and Supabase.

Feature Development Manual (For Humans & Autonomous Agents)

BoringPush follows a strict Data-First, Locality-Driven Architecture. Whether you are a solo founder building late at night or an autonomous AI agent working from mobile via SSH, you must follow the deterministic protocol below.

Prime Directive: Schema across all three databases → Provider registry delegation → Thin API route (≤40 lines) → Presentational UI. A session that touches JSX before database schemas are verified is an error.


The 5 Immutable Laws

  1. Tri-Database Parity: Every entity must exist with identical field names (camelCase, quoted in SQL), identical constraints, and identical lookup indexes across:
    • SQLite: providers/db/sqlite/client.js
    • MongoDB: providers/db/mongodb/models.js
    • Supabase: providers/db/supabase/schema.sql
  2. Zero Package Hallucinations: Never run npm install or add external npm packages. BoringPush uses built-in Node.js / Next.js capabilities and pure Tailwind CSS.
  3. Provider Registry Delegation: The app never imports database drivers directly. Everything delegates through providers/db/index.js and one-line re-export shims in models/<Entity>.js.
  4. Thin Routes (≤ 40 lines): API handlers in app/api/ only validate input length via LIMITS, authenticate via requirePaidUser(), delegate to a single provider call, and return JSON.
  5. Zero-Warning Verification Gate: Every modified file must pass node --check and npm run lint with 0 errors and 0 warnings before finishing.

The 5-Phase Feature Blueprint

┌─────────────────────────────────────────────────────────────┐
│ 1. SCHEMA PARITY  → Implement entity on SQLite/Mongo/Supa    │
│ 2. REGISTRY WIRE  → providers/db/<engine> + models/<E>.js   │
│ 3. THIN ROUTE     → app/api/<route>/route.js (≤ 40 lines)   │
│ 4. CLEAN VIEW     → components/<Feature>.js (Tailwind only)  │
│ 5. VERIFY GATE    → node --check + npm run lint + curl       │
└─────────────────────────────────────────────────────────────┘

Phase 1: Schema Parity (The Tri-Engine Law)

When creating a new entity (e.g. Bookmark), declare the schema across all three engines simultaneously.

1. SQLite (providers/db/sqlite/client.js)

Add the CREATE TABLE IF NOT EXISTS statement inside initRaw() and an additive ALTER TABLE guard:

db.exec(`
  CREATE TABLE IF NOT EXISTS "bookmark" (
    "id" TEXT PRIMARY KEY,
    "userId" TEXT NOT NULL,
    "url" TEXT NOT NULL,
    "title" TEXT NOT NULL,
    "createdAt" TEXT NOT NULL DEFAULT (datetime('now')),
    FOREIGN KEY ("userId") REFERENCES "user"("id") ON DELETE CASCADE
  );
  CREATE INDEX IF NOT EXISTS "idx_bookmark_user" ON "bookmark"("userId");
`);

2. MongoDB (providers/db/mongodb/models.js)

Add the Mongoose schema and lookup indexes:

const bookmarkSchema = new mongoose.Schema({
  userId: { type: String, required: true, index: true },
  url: { type: String, required: true, maxlength: LIMITS.urlLen },
  title: { type: String, required: true, maxlength: LIMITS.titleLen },
  createdAt: { type: Date, default: Date.now },
});

export const Bookmark = mongoose.models.Bookmark || mongoose.model("Bookmark", bookmarkSchema);

3. Supabase (providers/db/supabase/schema.sql)

Add the PostgreSQL table, indexes, and Row Level Security bypass for the service role:

CREATE TABLE IF NOT EXISTS "bookmarks" (
  "id" UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  "userId" TEXT NOT NULL,
  "url" TEXT NOT NULL,
  "title" TEXT NOT NULL,
  "createdAt" TIMESTAMPTZ NOT NULL DEFAULT NOW()
);

CREATE INDEX IF NOT EXISTS "idx_bookmarks_user" ON "bookmarks"("userId");
ALTER TABLE "bookmarks" ENABLE ROW LEVEL SECURITY;

Phase 2: Provider Surface & Re-Export Shims

Each database provider exposes identical function signatures returning plain objects { _id, id, ... }. Never leak driver-specific instances.

In providers/db/sqlite/bookmarks.js:

export function createBookmark(db, { userId, url, title }) {
  const id = crypto.randomUUID();
  const stmt = db.prepare(`
    INSERT INTO "bookmark" ("id", "userId", "url", "title")
    VALUES (?, ?, ?, ?)
  `);
  stmt.run(id, userId, url, title);
  return { id, _id: id, userId, url, title };
}

export function listBookmarksByUser(db, userId) {
  return db.prepare(`SELECT * FROM "bookmark" WHERE "userId" = ?`).all(userId);
}

In providers/db/index.js (The Facade):

export const Bookmark = {
  create: async (data) => (await getDbProvider()).Bookmark.create(data),
  listByUser: async (userId) => (await getDbProvider()).Bookmark.listByUser(userId),
};

In models/Bookmark.js (One-Line Shim):

// Frozen re-export shim — app code imports from @/models/Bookmark
export { Bookmark as default } from "@/providers/db";

Phase 3: Thin API Route (≤ 40 Lines)

API routes in app/api/ contain zero business logic. They perform four steps:

  1. Validate input against LIMITS from libs/config.js.
  2. Check authorization via requirePaidUser().
  3. Delegate to a single provider call.
  4. Return a clean JSON response.
// app/api/bookmarks/route.js
import { NextResponse } from "next/server";
import { requirePaidUser } from "@/libs/paywall";
import Bookmark from "@/models/Bookmark";
import { LIMITS } from "@/libs/config";

export async function POST(req) {
  const auth = await requirePaidUser();
  if (!auth.hasAccess) {
    return NextResponse.json({ error: "Paid plan required" }, { status: 403 });
  }

  const { url, title } = await req.json();
  if (!url || url.length > LIMITS.urlLen || !title || title.length > LIMITS.titleLen) {
    return NextResponse.json({ error: "Invalid input" }, { status: 400 });
  }

  const item = await Bookmark.create({ userId: auth.user.id, url, title });
  return NextResponse.json({ item }, { status: 201 });
}

Phase 4: Presentational UI

Place components in components/<Feature>.js.

  • Tailwind Only: Use standard utilities. No custom CSS frameworks or npm icon packages.
  • Stone Palette: Neutral, restrained stone tones (bg-stone-50, border-stone-200/80, text-stone-900, dark mode dark:bg-stone-900 dark:border-stone-800).
  • Props Down, Events Up: No direct database imports or provider calls inside client components.
// components/BookmarkCard.js
export default function BookmarkCard({ bookmark, onDelete }) {
  return (
    <div className="flex items-center justify-between p-4 rounded-xl border border-stone-200/80 dark:border-stone-800 bg-white dark:bg-stone-900">
      <div>
        <h3 className="text-sm font-medium text-stone-900 dark:text-stone-100">{bookmark.title}</h3>
        <p className="text-xs text-stone-500 truncate max-w-sm">{bookmark.url}</p>
      </div>
      {onDelete && (
        <button
          onClick={() => onDelete(bookmark.id)}
          className="text-xs text-stone-400 hover:text-red-500 transition-colors"
        >
          Delete
        </button>
      )}
    </div>
  );
}

Phase 5: Verification Gate (Mandatory)

Before declaring any feature complete or pushing to production, you must verify:

# 1. Syntax check on every modified JavaScript file
node --check app/api/bookmarks/route.js
node --check providers/db/sqlite/client.js

# 2. Entire repository lint (Must exit with code 0 and ZERO warnings)
npm run lint

# 3. Test HTTP boundary paths
curl -s -X POST http://localhost:3000/api/bookmarks \
  -H "Content-Type: application/json" \
  -d '{"url":"https://example.com","title":"Test"}'

Autonomous Agent Instructions

When an autonomous coding agent (Antigravity, Claude Code, Cursor) executes a user request to add or modify a feature:

  1. Do not create routes before schemas exist. Always define the entity across SQLite, MongoDB, and Supabase first.
  2. Never install external npm dependencies. If you need UUIDs, use crypto.randomUUID(). If you need date manipulation, use native Date.
  3. Keep routes under 40 lines. If a route exceeds 40 lines, extract helper functions into providers/ or libs/.
  4. Never run destructive migrations. Never execute DROP TABLE or DROP COLUMN in production scripts. Always use safe, additive ALTER TABLE ... ADD COLUMN statements with fallback defaults.
  5. Verify the gate: Always run npm run lint before finishing. If there are any ESLint warnings or errors, resolve them immediately.