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
- 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
- SQLite:
- Zero Package Hallucinations: Never run
npm installor add external npm packages. BoringPush uses built-in Node.js / Next.js capabilities and pure Tailwind CSS. - Provider Registry Delegation: The app never imports database drivers directly. Everything delegates through
providers/db/index.jsand one-line re-export shims inmodels/<Entity>.js. - Thin Routes (≤ 40 lines): API handlers in
app/api/only validate input length viaLIMITS, authenticate viarequirePaidUser(), delegate to a single provider call, and return JSON. - Zero-Warning Verification Gate: Every modified file must pass
node --checkandnpm run lintwith 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:
- Validate input against
LIMITSfromlibs/config.js. - Check authorization via
requirePaidUser(). - Delegate to a single provider call.
- 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 modedark: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:
- Do not create routes before schemas exist. Always define the entity across SQLite, MongoDB, and Supabase first.
- Never install external npm dependencies. If you need UUIDs, use
crypto.randomUUID(). If you need date manipulation, use nativeDate. - Keep routes under 40 lines. If a route exceeds 40 lines, extract helper functions into
providers/orlibs/. - Never run destructive migrations. Never execute
DROP TABLEorDROP COLUMNin production scripts. Always use safe, additiveALTER TABLE ... ADD COLUMNstatements with fallback defaults. - Verify the gate: Always run
npm run lintbefore finishing. If there are any ESLint warnings or errors, resolve them immediately.