Building Since — Self-Hosted Claim Tracker with PocketBase and React

Every prediction someone makes online deserves accountability. We built Since — a self-hosted temporal claim tracker — to register predictions, record past events, attach evidence, open disputes, and verify outcomes over time. The stack is deliberately lightweight: React 19, PocketBase, Express.js, and Docker. Here’s why we chose each piece, what surprised us, and what we’d change.

TL;DR

Since is a self-hosted claim tracker built with React 19, PocketBase, and Express.js. PocketBase replaced PostgreSQL, Redis, and a custom auth system — all in a single binary. The Express server acts as a backend-for-frontend (BFF), proxying API calls to PocketBase while handling Vite SSR. Collection-level access rules replaced traditional RLS. The entire stack deploys with docker compose up and runs on a single VPS. Try the live instance.

What You’ll Learn

  • PocketBase as a Database

    Schema-as-code, built-in auth, and collection-level access rules — no separate DB or auth server.

  • BFF Pattern with Express

    Express.js sits between React and PocketBase, handling API routing and Vite SSR middleware.

  • Claim → Evidence → Dispute

    A three-entity data model for tracking predictions, attaching proof, and challenging outcomes.

Why Build a Claim Tracker?

Pundits make predictions. Politicians make promises. CEOs make projections. Most of them are never held accountable. Since is a temporal accountability engine: ingest a claim, attach evidence, open disputes, and track outcomes over time.

The concept is simple but the implementation required careful thinking about data modelling, access control, and the lifecycle of a claim from creation through resolution.

The core workflow:

  • Register a claim — title, quote, claimant, event date, category, type
  • Set a deadline — optional prediction target date for future predictions
  • Submit evidence — URLs with descriptions and review status (pending/approved/rejected)
  • File disputes — challenge a claim’s validity with reasoning
  • Resolve outcomes — admin-only resolution: ACHIEVED, FAILED, DISPUTED, or MEME
Claim lifecycle workflow from registration to resolution

The Stack: Why PocketBase?

We evaluated three approaches before committing:

Stack comparison — PostgreSQL vs Firebase vs PocketBase

PocketBase won because it eliminated three separate concerns: database, authentication, and API layer — all in a single Go binary with an embedded SQLite database. The schema is defined as JSON and importable via the admin UI. No migrations, no ORM, no connection pool tuning.

PocketBase Collections as Schema

The entire data model lives in a single JSON file. Four collections: claims, evidence, disputes, and users. Import it via Settings → Import collections and you’re running. Here’s the simplified structure:

{
  "name": "claims",
  "type": "base",
  "listRule": "",              // Public read access
  "createRule": "auth_required", // Must be logged in
  "updateRule": "owner_only",   // Only claim creator
  "deleteRule": "owner_only",
  "fields": [
    { "name": "uuid", "type": "text" },
    { "name": "title", "type": "text" },
    { "name": "quote", "type": "text" },
    { "name": "claimant", "type": "text" },
    { "name": "category", "type": "select" },
    { "name": "type", "type": "select" },
    { "name": "outcomeStatus", "type": "select" }
  ]
}

This replaced what would normally be a PostgreSQL schema with RLS policies, a separate auth service, and an ORM layer. The trade-off: PocketBase’s access rules are simpler than RLS but less flexible for complex queries.

Access Rules as Authorization

PocketBase’s collection-level rules handle authorization without a separate middleware layer. Each collection defines who can read, create, update, and delete records. Rules can reference the current auth context to enforce ownership checks and role-based access. The key pattern: separate auth collections for regular users vs admins, with rules that check the collection name.

Architecture: Express as BFF

The Express server acts as a backend-for-frontend. It handles two concerns: API routing and Vite SSR middleware. The React frontend talks to Express, which proxies to PocketBase.

Architecture diagram — React to Express BFF to PocketBase
// Simplified server structure
import express from 'express';
import PocketBase from 'pocketbase';

const pb = new PocketBase(process.env.POCKETBASE_URL);

// API routes proxy to PocketBase
app.get('/api/v1/claims', async (req, res) => {
  const result = await pb.collection('claims').getList(1, 100);
  res.json({ data: result.items, status: 'success' });
});

// Vite SSR middleware for non-API routes
app.use((req, res, next) => {
  if (req.path.startsWith('/api/')) return next();
  return vite.middlewares(req, res, next);
});

This pattern gave us the best of both worlds: Vite’s fast development experience with a clean API boundary. In production, the same server handles both — no separate build step for the API.

Why Not Call PocketBase Directly from React?

PocketBase has a JavaScript SDK and could be called directly from the browser. We chose the BFF pattern for three reasons:

  • Superuser operations — Some operations need admin access that shouldn’t be exposed to the client
  • API versioning — The Express layer lets us version the API independently of PocketBase’s internal structure
  • SSR capability — Vite’s SSR middleware needs a server to run on

The Data Model

Three core entities, each with a clear lifecycle:

Data model — Claims, Evidence, and Disputes entities

Claims have six categories (Regulatory, Deterministic, Observational, Index Fund, User Driven, Viral Signal), three types (Past Event, Prediction, Deadline), and five outcome statuses (Unresolved, Achieved, Failed, Disputed, Meme). UUID-based routing means claims are shareable by link without exposing internal IDs.

Frontend: React 19 + Tailwind + Motion

The frontend is a single-page application with five routes: Home (all claims), Trending, Watchlist (user’s claims), Category filter, and Claim Detail. The UI uses a sci-fi aesthetic — dark backgrounds, amber accents, monospaced labels, and subtle animations.

Key Component: TimerDisplay

The countdown timer is the most visual element. For predictions, it shows time remaining until the target deadline. For past events, it shows time elapsed since the event. Built with date-fns and updated every second.

Key Component: StatusBadge

Color-coded outcome badges: green for Achieved, red for Failed, amber for Disputed, neutral for Unresolved, and a special status for Meme. The badge appears on both the card grid and the detail page.

Animations use the motion library (Framer Motion’s successor). Page transitions, form reveals, and card hover states are all animated. The AnimatePresence component handles layout animations when the claim grid re-renders.

Deployment: Docker Compose

The entire stack deploys with two containers:

# docker-compose.yml
services:
  frontend:
    image: nemesisguy/since-frontend:latest
    ports:
      - "3088:3000"
    depends_on:
      - pocketbase

  pocketbase:
    image: ghcr.io/muchobien/pocketbase:latest
    ports:
      - "3089:8090"
    volumes:
      - pb_data:/pb_data

The frontend container runs the Express server (which serves both the API and the Vite-built React app). PocketBase runs as a separate container with persistent volume storage. Total deployment: docker compose up -d.

The Dockerfile is a single-stage build — install deps, build frontend, run server:

FROM node:20-alpine
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build
EXPOSE 3000
CMD ["node", "server.mjs"]

Lessons Learned

Key lessons learned building Since with PocketBase

Win: PocketBase Eliminated Three Services

What would normally be PostgreSQL + Express auth + Redis sessions became a single Go binary. The schema-as-code approach means the entire database definition is version-controlled and importable. No migrations, no ORM, no connection pool tuning. The built-in admin UI gives you a dashboard for free.

Lesson: PocketBase’s Superuser Auth Pattern

The Express server authenticates as a PocketBase superuser on every request. This is intentional — the BFF pattern means the client never touches PocketBase directly. But it means the server re-authenticates on each API call, which adds latency. For a production system with higher traffic, we’d cache the auth token and handle refresh rotation.

Trade-off: SQLite vs PostgreSQL

PocketBase uses SQLite, which means single-server writes. For Since’s use case (low write volume, high read volume), this is fine. But if we needed multi-region writes or concurrent high-throughput ingestion, we’d need to migrate to PostgreSQL.

Lesson: Schema Evolution Without Migrations

PocketBase lets you add fields via the admin UI or API without formal migrations. The server auto-adds missing fields on startup. This is great for rapid iteration but means schema changes aren’t tracked in version control unless you manually export the collections JSON.

What’s Next

  • Evidence review workflow — Moving from Pending/Approved/Rejected to a more granular review system with reviewer notes
  • Real-time updates — PocketBase supports WebSocket subscriptions; we’ll use them for live claim updates
  • Public API — Opening the claims data via a read-only API for researchers and analysts
  • Mobile app — React Native wrapper around the existing API for on-the-go claim submission

Final Thoughts

Since taught us that PocketBase is production-ready for the right use case. It’s not a replacement for PostgreSQL in every scenario — but for single-server applications with moderate traffic, it eliminates an enormous amount of infrastructure complexity.

The claim tracker concept is simple, but the accountability it creates is powerful. Every prediction has a timestamp, every outcome has evidence, and every dispute has a paper trail. That’s the kind of system the internet needs more of.

If you’re building a self-hosted application and want to discuss architecture decisions, let’s talk. We’ve been through the PocketBase gotchas, the Express BFF patterns, and the Docker deployment puzzles — so you don’t have to.

Ready to Build Something?

We design and deploy self-hosted applications for South African businesses. Whether you’re building a claim tracker, a SaaS platform, or an internal tool, our team can help you choose the right stack.

Related Reading