NexTwelve Sign in

Technical architecture

A small, sturdy stack for a planning system of record

NexTwelve runs on Cloudflare Workers in front of Postgres. One API is the only way to change the plan: it checks who may change what, writes the cells and records the audit trail in a single database transaction.

1write path for every change: entered, imported, restored or undone
100%of tenant tables under row-level security, enforced by Postgres
500+automated tests against a real Postgres, most of them adversarial
0floating-point values: amounts are exact decimals end to end

Overview

Three tiers. Clients talk only to the Worker; the Worker is the only thing that talks to the database and to file storage.

Dashed outline: planned. Everything else is built and tested.

Components

LayerTechnologyResponsibility
APICloudflare Workers, Hono, TypeScriptEvery request: session check, input validation, reads and writes in one transaction each. The same code runs in Node for tests and scripts.
DatabasePostgres 16 (PlanetScale Postgres or any managed Postgres)The plan itself. Schema epm, row-level security, and the database functions that perform every write.
ConnectionsCloudflare HyperdriveKeeps connections to Postgres warm near the database. Caching is off so every read sees the latest committed values.
FilesCloudflare R2Import files, reference workbooks and snapshots, under one prefix per tenant.
Scheduled jobsCron TriggersNightly snapshots and retention; hourly cleanup of expired demo copies. Authorized by a job key whose hash is the only copy in the database.
IdentityGoogle sign-inA Google ID token is exchanged for a signed 15-minute session token. Invitations, roles and access live in NexTwelve.
Web appHTML, CSS and plain JavaScriptNo framework and no build step; served as static assets with a strict Content Security Policy.
Bot checkCloudflare Turnstile (optional)In front of "Try the live demo" only.

Data model

Applications, cubes and dimensions

An application holds dimensions and cubes. Every cube has Scenario, Version, Years, Period, Entity and Account; Department and up to eight custom dimensions (Product, Channel and so on) are optional.

Members and hierarchies

Members form hierarchies in which each member adds to, subtracts from or is ignored by its parent. Deploying checks the hierarchies and rebuilds a closure table of every ancestor and descendant, so any total is one join away.

Cells

Only entered or loaded cells are stored, one row each, keyed by a 16-slot coordinate: cube, the six fixed dimensions, Department and eight custom slots. Values are numeric(24,6): exact decimals, never floating point.

History

An audit row for every changed cell (old and new value), a transaction record in a SHA-256 hash chain, and versioned members, forms and reports with the moment each change took effect.

How a change is saved

Grid saves, imports, undo and restores all take this path. There is no other way to write a cell: the API's database role has no direct write access to cells, audit or access tables.

  1. The client sends the changed cells with its session tokenAlong with the values it read, so the server can tell whether someone else changed them since.
  2. The Worker opens one transaction and sets the tenant and userFrom here on, Postgres itself confines every query to that tenant.
  3. apply_changes checks everything again, inside the databaseRole, coordinates, write access to each member, open plan windows, and conflicts with changes made meanwhile.
  4. Cells, audit rows and the chain entry are written togetherOne audit row per changed cell; one transaction record linked to the previous one by a SHA-256 hash.
  5. Commit, or nothing at allTotals are computed on read from the closure table, so they are never stale. Undo and point-in-time restore replay old values from the audit trail.

Security

LayerHow it is enforced
IdentityGoogle ID tokens checked for signature, issuer, audience and expiry. Users match on Google's stable ID; invitations bind by verified email; a Workspace domain can admit its own users.
Tenant isolationRow-level security on every tenant table, forced so that database functions are bound by it too. The API connects as a role that owns nothing and cannot bypass it.
RolesService administrator per tenant; application administrator, power user, planner and viewer per application. Checked in the API and again in the database.
Member-level accessRead and write grants on secured dimensions, to users or groups. The closest grant wins and an explicit "none" denies. Forms and reports show only what a person may read.
WritesOnly through apply_changes, which re-checks role, access and plan windows for every cell.
AuditAppend-only cell audit and a per-application hash chain that can be re-verified at any time.
PhonesPaired by a one-time QR code (two minutes) that the user approves on the computer by typing the number the phone shows. The phone's session may call only read routes, its database transactions are read-only, and it ends when revoked from either side or after 8 hours.
Files and snapshotsStored under the tenant's prefix; snapshot files carry SHA-256 checksums kept in the database, and a restore refuses any file that does not match.

Backup and recovery

Point-in-time restore

Put a slice of data (cube, scenarios, versions, any members) back to how it was at a moment, in place or into another version. Preview first; up to 100,000 changed cells per restore.

Metadata restore

Put members, hierarchies, forms and reports back to a moment or a snapshot. Deleted members return with their history; members added since that hold data are kept.

Snapshots

A consistent copy of an application in R2: manual, nightly (newest 30, plus one a month for 12 months) and automatically before snapshot and metadata restores. Restore in place or into a new application.

Every restore is itself an audited change that can be undone.

Public demo

A private copy per visitor

  • Each visitor gets a tenant of their own, loaded from a stored template in a few seconds
  • Three people in every copy: administrator, regional planner, viewer
  • Isolated by the same row-level security as real tenants
  • Removed with its files when it expires (24 hours) or when the visitor starts over

Guard rails

  • No inviting people, changing roles or creating applications
  • 1 MB per request, 16 MB of uploads and 20 snapshots per copy
  • Limits per network on new copies per hour and live copies at once, plus an overall cap
  • An optional Turnstile check before a copy starts

Core MVP requirements

What the minimum viable product had to do, and how each requirement is met and verified.

IDRequirementHow it is met and verifiedStatus
F1Model a business: dimensions, hierarchies, cubesSix fixed dimensions, optional Department, eight custom; validated deploy; metadata test suitesMet
F2Enter plan data in formsSaved forms, page choices, up to three dimensions per axis, paste, conflict detection; grid and form suitesMet
F3Report and exportReports with suppression, scaling and rounding; Excel and CSV output; report suitesMet
F4Load actuals from the general ledgerCSV and Excel imports, mapping rules, crosswalk workbooks, dry runs, drill-back; import suitesMet
F5Control who sees and changes whatRoles, member-level grants, plan windows; security and isolation suitesMet
F6Audit every change and undo itCell audit, hash chain with verification, undo by transaction; grid and concurrency suitesMet
F7Restore data and metadata to a point in timeSlice restore and metadata restore with previews; restore and metadata-restore suitesMet
F8Back up applicationsManual, nightly and pre-restore snapshots with retention and checksums; snapshot suitesMet
F9Edit form and report definitions in ExcelExport and import as Excel or CSV, checked row by row before anything changes; definitions suitesMet
F10Plan in Google SheetsSheets add-on on the same APIPhase 1
N1Tenant isolation that does not depend on the APIForced row-level security; tests read every tenant table as another tenant and as no tenantMet
N2Exact amountsDecimal storage and decimal text in snapshots, exports and reports; precision suitesMet
N3One audited write pathThe API role cannot write cells, audit or access tables directly; checked by the isolation suiteMet
N4Responsive at sample-company scale20,000-cell saves in seconds on a populated application, asserted in tests; a demo copy starts in a few secondsMet
N5RecoverableNightly snapshots plus point-in-time restore from the audit trail; tampered and missing files are refusedMet
N6Hard to break500+ tests against a real Postgres, about 390 of them adversarialMet
N7Self-hostableOpen source; runs locally with Docker and Wrangler, deploys to your own Cloudflare account and PostgresMet

Limits

WhatLimit
Cells in one grid, form or report50,000
Dimensions on a form or report axis3
Custom dimensions per cube8
Amount precision18 digits before the decimal point, 6 after
Changed cells per restore100,000
Uploaded file25 MB (1 MB in the demo)
Mapping rules per import format50,000
Definitions import5,000 definitions and 100,000 layout rows per file
Session15 minutes (a demo session lasts as long as its copy)
Snapshot retentionNightly: newest 30 plus newest per month for 12 months. Before a restore: 90 days. Manual: kept.

Read the code, run it yourself

The schema, the API, the browser app and every test are in the repository.

NexTwelve on GitHub