
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

The Stack: Why PocketBase?
We evaluated three approaches before committing:

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.

// 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:

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

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
- TorqueBooks: Workshop Management System — Another self-hosted SaaS with PocketBase and Keycloak
- Runtime Env Injection for Docker Frontends — How we deploy Dockerized frontends across environments
- Self-Hosted CI/CD on a Home Rack — The deployment pipeline that ships Since
- From Rebuilding Auth to a Shared Identity Layer — The auth architecture PocketBase simplified
- Custom Software Development — Self-hosted applications with modern stacks