M0: foundation — monorepo, PostGIS schema, deck query, app shell

Greenfield scaffold for Linkder, a swipe-to-hire marketplace for local
professional services.

- pnpm/turbo monorepo: apps/web, packages/{shared,db}
- Postgres 16 + PostGIS via docker compose (ports 5442/6389 to avoid
  clashing with other local stacks)
- Drizzle schema, 23 tables, geography(Point,4326) with GiST indexes
- Domain core in packages/shared: integer-cent money, status transition
  graphs, deck ranking weights, cancellation policy — 46 unit tests
- Deck query: filtering in Postgres on the GiST index, ranking in JS so
  the weights stay tunable — 18 integration tests against a seeded DB
- Deterministic seed placing pros at known distances, including three
  that must NOT appear on a deck (out of radius, unverified, away)
- Next.js 15 app shell with a working swipe deck
- CI: typecheck, lint, test, build against live postgres+redis

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
serfowi
2026-08-20 13:32:35 -04:00
co-authored by Claude Opus 5
commit 19623bcccb
66 changed files with 12412 additions and 0 deletions
+73
View File
@@ -0,0 +1,73 @@
import { drizzle, type PostgresJsDatabase } from 'drizzle-orm/postgres-js';
import postgres from 'postgres';
import * as schema from './schema/index';
/**
* The pool and the Drizzle instance are created on first use, not on import.
*
* Eager construction would make `next build` fail on any machine without a
* database — including CI, which only needs to compile pages. Failing on first
* query instead keeps the failure where it is actionable.
*
* The instance is cached on globalThis so Next's dev server does not open a new
* pool on every hot reload and exhaust Postgres connections within a minute.
*/
const globalForDb = globalThis as unknown as {
__linkderPool?: postgres.Sql;
__linkderDb?: PostgresJsDatabase<typeof schema>;
};
function createPool(): postgres.Sql {
const connectionString = process.env.DATABASE_URL;
if (!connectionString) {
throw new Error(
'DATABASE_URL is not set. Copy .env.example to .env and start Postgres with `pnpm services:up`.',
);
}
return postgres(connectionString, {
max: Number(process.env.DB_POOL_MAX ?? 10),
idle_timeout: 20,
});
}
export function getPool(): postgres.Sql {
const existing = globalForDb.__linkderPool;
if (existing) return existing;
const created = createPool();
globalForDb.__linkderPool = created;
return created;
}
function getDb(): PostgresJsDatabase<typeof schema> {
const existing = globalForDb.__linkderDb;
if (existing) return existing;
const created = drizzle(getPool(), { schema });
globalForDb.__linkderDb = created;
return created;
}
export type Db = PostgresJsDatabase<typeof schema>;
/**
* Lazily-initialised database handle. Behaves exactly like a Drizzle instance;
* the connection is only opened when a property is first touched.
*/
export const db: Db = new Proxy({} as Db, {
get(_target, prop, receiver) {
return Reflect.get(getDb() as object, prop, receiver);
},
has(_target, prop) {
return Reflect.has(getDb() as object, prop);
},
});
/** Close the pool. For scripts and test teardown — never call this from a request. */
export async function closePool(): Promise<void> {
const existing = globalForDb.__linkderPool;
if (!existing) return;
await existing.end();
globalForDb.__linkderPool = undefined;
globalForDb.__linkderDb = undefined;
}
export { schema };