Skip to content
owen/richards
Home

Flagship · Solo build · Oct 2025 – present

FieldDay

A 13-domain monorepo where the architecture rules are executable, not aspirational.

A pre-launch participation platform for People, Organizations, and Sponsors, built solo as one Nx monorepo. Bun + Express + tRPC + Zod over PostgreSQL/PostGIS, where domain isolation, a financial ledger, youth safety, and even product vocabulary are enforced by custom ESLint rules and CI ratchets that fail the build, not by code review.

13 / 14
Backend domains / frontend packages, one Nx monorepo
604
PostgreSQL tables · 429 migrations
136
Architecture checks that fail the build with a reason
6
Custom ESLint rules enforcing DDD boundaries
1536-dim
halfvec HNSW embeddings for hybrid search
3,700+
Commits, solo, since Oct 2025

What it is

One participation graph, three co-primary realms, one app.

Most active-life software owns one slice: Strava the solo athlete, TeamSnap the youth-sports family, Mindbody the studio. FieldDay models the whole graph: the people, the organizations that run activities, and the sponsors that fund them, in a single product.

People

/people/*

Free, with no paywalled coordination, ever

Organizations

/org/*

Paid SaaS + payments take-rate (the revenue engine)

Sponsors

/sponsor/*

Operator-ready; revenue is 2028+ optionality

Pre-launch and local-dev: the design is complete (80 canonical vision docs) and execution runs toward a Phase 1 ship target of December 31, 2026. Everything below is labelled Built / Flag-gated / Designed so the line between shipped code and roadmap is never blurred.

The idea worth stealing

Architecture enforced as code: 6 ESLint rules + 136 architecture checks.

The rules that keep a large system coherent are usually a wiki page nobody reads. Here they are executable. Six custom ESLint rules and 136 architecture checks (96 on every push) fail the build the moment an invariant is violated. Boundaries aren't a convention; they're a compiler error.

packages/config/eslint/rules/no-cross-domain-import.js
// Derives the source domain from the file path and regex-matches the import —
// deliberately NOT the Nx project graph, which silently skips when the graph plugin OOMs:
"Boundary enforcement that disappears when CI is unhappy is not enforcement."
  • Six custom ESLint rules. no-cross-domain-import, no-database-in-router (the Router→Service→Repository spine), no-raw-event-bus-on (forces a never-throw listener wrapper), plus design-token guards (no-raw-hex, no-arbitrary-tailwind, no-raw-motion-import).
    Built
  • Ninety-six fast architecture ratchets run in pre-push.sh (FK-index coverage, transaction discipline, tenant-scoped queries, soft-delete filters, test coverage), each baseline-tracked, so technical debt can only go down, never silently up.
    Built
  • Doctrine as code. vocabulary-check and pricing-check ban deprecated product terms (Class→Program, Booking→Registration, Payment→Transaction). Product vocabulary is a compile-time invariant.
    Built

Skills shown

  • Custom ESLint / AST rules
  • Architecture-as-code
  • CI gate design
  • Fitness functions
  • DDD boundaries

System architecture

Domain-driven, event-bus-only, type-safe end to end.

Thirteen bounded contexts in one workspace. They never import each other (cross-domain communication flows over an event bus), and every service follows the same spine: Router → Service → Repository. tRPC + Zod give a single source of truth from PostgreSQL to React, with explicit return types throughout and no any.

One Next.js app serves three realms; a Bun + tRPC API fronts a shared core and 13 domains that talk only over an event bus.

Platform
Next.js 16 app
/people · /org · /sponsor
API
Bun + Express + tRPC
Zod-validated · versioned
Core
backend-core
db · cache · event bus · errors
Domains
13 isolated domains
Router → Service → Repository
Data
PostgreSQL + PostGIS
DragonflyDB
cache
Typesense + pgvector
hybrid search
MinIO / S3
objects

Enforced · Domains can't import each other: 6 custom ESLint rules and 136 architecture checks fail the build, not the code review.

Architecture · One Nx monorepo · 13 domains · 604 tables · 429 forward-only migrations

The 13 backend domains

people

Auth, profiles, households, trust scoring, circles

activities

Sessions, programs, registrations, discovery, search

commerce

Payments, ledger, subscriptions, payouts, dunning

facilities

Venues, spaces, bookings, staff

engagement

Notifications, messaging, reviews, squads, feed

safety

Waivers, incidents, background checks, COPPA consent

authz

Role graph, RBAC, capabilities

sponsors

Campaigns, targeting, impact, storefront (40+ routers)

infrastructure

Analytics, jobs, realtime, outbox, tracing

integrations

Booking providers, public APIs, webhooks

tenants

Multi-tenant config, custom domains

calendar-sync

Calendar integration (service-only)

widgets

Embeddable widget analytics

Skills shown

  • Domain-Driven Design
  • Event-driven architecture
  • Nx monorepo
  • tRPC + Zod e2e types
  • Layered design

The money system

An append-only ledger, a take-rate engine, and the judgment to reject TigerBeetle.

  • Append-only transaction ledger. commerce.transactions is never mutated. It uses quarterly RANGE partitioning on created_at, a composite primary key, and a ledger_source enum (postgres | tigerbeetle) for dual-write tracking.
    Built
  • Differential take-rate. Fees key on a discovery_source enum (org-direct vs. fieldday-discovered vs. sponsor-funded) × tier → markup in basis points, with a DB-level invariant: amount = facilityCut + platformFee + net.
    Built
  • Dunning state machine. Failed payments retry on an escalation cascade (email → SMS → WhatsApp → in-app), idempotent on provider_message_id, with TCPA quiet-hours skips and a hard retry_count < 3 ceiling.
    Built
  • TigerBeetle, deliberately deferred. A 795-line evaluation rejects a dedicated financial DB for general transactions (Postgres is correct at ~3,000 tx/month; TigerBeetle only matters near 5,000 tx/sec) and pilots it only as a shadow ledger for sponsor flows, gated behind seven clean days of staging reconciliation.
    Designed
packages/backend/domains/commerce/sponsor-ledger/account-ids.ts
// UUIDv7's non-zero top byte made the original XOR namespace tag collide ~1/256.
// Fix: clear the top 8 bits, then OR a fixed tag — misrouting becomes impossible.
const BOTTOM_120_MASK = (1n << 120n) - 1n;
account_id = (uuidToU128(sponsor.id) & BOTTOM_120_MASK) | POOL_TAG; // 0x01 pool, 0x02 payable

PCI DSS 4.0.1 tooling Built: a weekly leader-elected job (Postgres advisory lock) snapshots payment-page CSP headers and script integrity, routing drift by severity (forensic vs. warning vs. page-the-on-call) into an immutable audit.security_events trail.

Skills shown

  • Financial ledger design
  • Postgres partitioning
  • Stripe Connect
  • Billing / take-rate engine
  • Distributed ID design (u128)
  • Build-vs-buy judgment
  • PCI DSS compliance

Search & ML infrastructure

pgvector + Typesense, fused by Reciprocal Rank Fusion.

  • Production vector store. halfvec(1536) embeddings with HNSW indexes (m=16, ef_construction=100, halfvec_cosine_ops) plus three entity-scoped partial indexes for filtered vector search at million scale.
    Built
  • Hybrid retrieval. Typesense keyword results and pgvector cosine results are merged by Reciprocal Rank Fusion (score(d) = Σ 1/(k + rank), k = 60), shipped in semantic-search.service.ts.
    Built
  • ML observability in the schema. A model registry, append-only prediction logs, drift checks (PSI / KS / JS divergence), a feature store with TTLs, and per-user GDPR/CCPA ML-consent tracking.
    Built
  • AI operator services, behind flags. Five AI-native features (scheduling, demand forecasting, churn, copy, attribution) are designed with non-AI fallbacks; the plan is explicit that there are zero Claude API calls in any router today. Bedrock model routing is a decision, not yet code.
    Designed

Skills shown

  • pgvector / HNSW ANN
  • Hybrid retrieval (BM25 + dense + RRF)
  • ML observability
  • Drift detection
  • Feature store

Trust, safety & access

Trust written once, youth safety enforced in the backend.

  • Trust written once, read everywhere. TrustTier is computed by exactly one service from five weighted factors (attendance 0.4, cancellation-notice 0.2, payment 0.2, account-age 0.1, verification 0.1). A Postgres trigger mirrors a CHECK constraint; every other realm reads the tier, never the raw score.
    Built
  • Youth safety is structural. Adult↔minor 1:1 DMs are blocked in the backend (SafeSport MAAPP), COPPA consent is required under 13, and guardian signatures are binding on minor waivers. These are guarantees the UI can't undo.
    Built
  • Capability-based RBAC. Effective capabilities are computed across realms and enforced at the tRPC boundary by requireCapability middleware, with an event-sourced audit outbox (aggregate version + sequence number, monotonic via GREATEST()).
    Built
  • OpenFGA / Zanzibar direction. A relation-based authorization model is being laid in (an entityRelationships table, 20 capability↔relation mappings). It's blueprinted, not yet the live path.
    Designed

Skills shown

  • Reputation systems
  • Single-source-of-truth
  • RBAC / capabilities
  • Relation-based authz
  • COPPA / SafeSport
  • Event sourcing + outbox

Worldwide-first data

Internationalization is a database constraint, not a backlog item.

  • Constraints, not conventions. Profiles enforce ISO 3166-1 country codes and E.164 phones via a CHECK regex; money is { amount: bigint, currency: ISO 4217 } (never a float), and 24 organization types are enumerated for multi-vertical support from day one.
    Built
  • 604 tables, 429 migrations. Drizzle is the source of truth (latest 0631); no rollbacks. High-volume tables are time-partitioned, IDs are UUIDv7, and every migration-hygiene rule is its own ratchet with a baseline.
    Built
  • Per-jurisdiction privacy regimes. GDPR / CCPA / LGPD / PIPA routing is doctrine-described and partly schema-routed by country code; external audits are deferred until 100K users by the data doctrine itself.
    Designed

Skills shown

  • i18n data modeling
  • Money as bigint
  • DB constraint design
  • Forward-only migrations
  • Partitioning / indexing
  • Privacy-by-design

Product doctrine

A refusal to ship the dark patterns the category relies on.

These are binding. They shape the schema and are enforced by the vocabulary and pricing ratchets, not left to product whim.

  • No loss-on-miss streaks: show “3 of the last 4 weeks,” never a punishing strike-out
  • No public vanity metrics: likes, follower and view counts are never shown to others
  • No infinite algorithmic feed: every feed is finite and ends with “you’re caught up”
  • No paywalled coordination: RSVP, calendar, messaging and the annual recap are always free
  • No rebrandable trust or safety: trust and auth surfaces are locked from tenant theming

The stack

Every version taken from a committed package.json.

Runtime & language

  • TypeScript 6 (strict)
  • Bun 1.3
  • Nx 22

API

  • Express 4 + tRPC 11
  • Zod
  • @fieldday/contracts

Data

  • PostgreSQL 18 + PostGIS
  • Drizzle 0.44
  • pgvector (halfvec/HNSW)
  • DragonflyDB
  • Typesense 27
  • MinIO / S3

Web & mobile

  • Next.js 16
  • React 19
  • TanStack Query
  • Tailwind · shadcn/ui
  • React Native

Payments & comms

  • Stripe Connect 19
  • Twilio
  • Socket.io
  • BullMQ

Quality & infra

  • Vitest · Playwright
  • OpenTelemetry + Pino
  • Terraform
  • Docker

Honest status

What's shipped, what's flag-gated, what's still a doc.

Built

  • The monorepo + 13 domains + the ESLint / ratchet enforcement layer
  • Append-only ledger, differential take-rate, dunning, PCI tamper tooling
  • pgvector + Typesense hybrid search, ML observability schema
  • Trust scoring, capability RBAC, backend-enforced youth safety, worldwide schema

Flag-gated

  • Sponsor v2 surface: 70+ routes behind sponsor.v2.* flags
  • Lifespan-persona acquisition behind gtm.* floors
  • Sponsor v1 is operator-ready; revenue is explicitly 2028+ optionality

Designed

  • Five AI operator services (zero Claude calls in any router today)
  • AWS Bedrock model routing; TigerBeetle production dual-write
  • Full OpenFGA cutover, multi-region infra, external privacy audits

This split is the point, not a disclaimer. The numbers above are taken from the live repository (429 migrations, latest 0631; 6 ESLint rules; halfvec(1536) at m=16), and a design doc is never dressed up as shipped code.

The systems half of a forward-deployed engineer.