Build plan v1.012 August 2026PostgreSQLOman first

Building InvoiceNext

Stack, infrastructure, repository, pipeline and the mission sequence — carrying over next2's parallel-fleet method and the eleven verification rules it paid for, with the deltas that PostgreSQL and a compliance product force.

Section 01

Tech stack

Deliberately close to next2 — the team knows it, the patterns are proven, and the reusable pure-Python modules drop straight in. Six things change, and each is argued in the next section.

LayerChoiceSame as next2?
Language / runtimePython 3.12Same
APIFastAPI + uvicorn, router auto-discovery per moduleSame
DatabasePostgreSQL 17 via psycopg 3Changed
Data accessSQLAlchemy 2.0 Core + ORM for mastersChanged
MigrationsAlembic, single chain, own alembic_versionSame
TenancyNative Postgres RLS + SET LOCAL app.tenant_idReworked
Background jobsPostgres queue, FOR UPDATE SKIP LOCKEDChanged
Authargon2-cffi + PyJWT, epoch-claim revocationSame
PDFWeasyPrint + Jinja2Same
FrontendReact 19 + Vite 6 + TypeScript 5.6Same
StylingCSS custom properties, no framework — the design-kit tokensChanged
i18nreact-i18next + ICU, en / arNew
Icons / typeFont Awesome 7 Pro (self-hosted) · IBM Plex Sans / Arabic / MonoPro is new
Testingpytest + vitest + PlaywrightPlaywright is new
Container / proxyDocker Compose + Caddy with automatic TLSSame
Excelopenpyxl, export short-circuit inside the grid runnerSame
Section 02

The six deltas, argued

1 · PostgreSQL, and therefore a fresh kernel

Native row-level security, no per-tenant licensing, and the right engine for thousands of small tenants on a free tier. The consequence is unavoidable and worth stating plainly: next2's kernel is raw T‑SQL and does not port. We take the design and the pure-Python modules; the tenancy layer is written fresh. Postgres RLS is also simply better here — SET LOCAL is transaction-scoped, so a pooled connection cannot leak tenant context the way SQL Server's session context can.

2 · SQLAlchemy Core rather than raw cursors

next2 issues raw SQL through pyodbc. That was defensible for an ERP with hand-tuned queries against a legacy schema; it is the wrong default for a greenfield product whose hardest queries are financial reports composed from filters. Core gives composable, typed SQL without the identity-map surprises of the full ORM — and the ORM earns its place only for master-data CRUD.

3 · The job queue lives in Postgres, not Redis

next2 runs dramatiq on Redis. We drop Redis entirely and use SELECT … FOR UPDATE SKIP LOCKED. The reason is not simplicity, though it removes a container: a job enqueued in the same transaction as the document it concerns cannot be lost or duplicated. For a compliance product where "we submitted twice" and "we never submitted" are both reportable failures, transactional enqueue is a correctness property, not an optimisation. The e-invoicing worker's state machine already assumes exactly this claim-and-advance shape.

4 · No CSS framework

The design kit is built on CSS custom properties and logical properties, and it mirrors to Arabic by setting one attribute. Tailwind would add a build dependency and handle RTL through variants — strictly worse for a product where every rule must mirror. next2's Bootstrap is not worth carrying either.

5 · i18n from the first commit

next2 has 2,500 hard-coded English strings across 263 components and no i18n library. That is the cost of retrofitting, measured. We extract from commit one, with pseudo-locale in CI so an unextracted string fails the build rather than surfacing in an Arabic screenshot.

6 · A Playwright suite, because next2 has none

Its *_e2e/ directories hold screenshot evidence, not tests. The rule that earned its place on that programme — dogfooding finds what unit tests structurally cannot — applies doubly here: RTL layout, bilingual PDFs and the wire-action confirmations are all invisible to a unit test.

Section 03

Infrastructure

A separate VM is not a preference. next2 runs one self-hosted CI runner with one job slot, and its own traffic already produces measured queue waits of 30 and 37 minutes against a 34.5-minute median. Sharing it would slow both products.

Revised 12 Aug — no new VM and no managed database. next2 is paused, so its server becomes our staging box and this development VM becomes the build machine. Both changes are already in place.

ComponentChoiceState
Staging servernext2app40.80.84.69, B2as_v2, centralindiaLive — 54 GB free, 5.1 GB RAM available
DevelopmentThis VM, project on /data/invoicenextMoved — 96 GB headroom
DatabasePostgreSQL 17 in a container, volume on diskMovable to managed on demand
TLS / proxyCaddy already running on next2appOne line per hostname
OrchestrationDocker Compose, separate project name from next2Own network, own volumes
CI runnerSecond runner on next2app, label invoicenextnext2's runner is idle while it is paused
Object storageinvoicenext storage account, container invoicenext, prefixes dev/ and prod/Created — see below
DNSapp.invoicenext.ai → 40.80.84.69Waiting on purchase
ProductionIts own VM, provisioned at launchDeferred until there is something to launch

Blob storage — provisioned 12 August

SettingValue
Account / containerinvoicenext / invoicenext — JOBNEXTINPROD, centralindia, Standard_LRS
Layoutdev/ and prod/
Public accessDisabled
TransportHTTPS only, TLS 1.2 minimum
Soft delete30 days
VersioningEnabled

Soft delete and versioning are not conveniences — they are the ISO 27001 evidence that a record cannot be quietly destroyed, and they support the fifteen-year retention obligation. An immutability policy goes on the prod/ prefix before the first live tenant, not now, because it cannot be undone.

Containerised Postgres — what it costs us, and how we pay it

Running our own database saves roughly $45 a month and keeps the option of moving to a managed service later. What it does not save us is the control: ISO 27001 wants tested backup and restore with evidence, and Oman wants ten years of records. So a nightly pg_dump to dev/backups/ and prod/backups/ lands in phase 1, not later — with a quarterly restore drill that is itself a CI job, so the evidence is generated rather than remembered. That is the whole reason the blob exists from day one.

Two housekeeping notes from setting this up

38.3 GB of Docker build cache reclaimed on next2app — its deploy script prunes images but never the builder, so free space went from 19 GB to 54 GB. Our deploy.sh carries docker builder prune from the first commit. Separately, next2app has no swap; now that two products share the box, 4 GB of swap is cheap insurance against an out-of-memory kill taking down a live site.

The risk of co-tenanting, stated plainly

next2app still serves app.jobnext.ai and app.projectsnext.ai, which are live. Our containers run under a separate Compose project with their own network and volumes, and the Caddy change is purely additive — but we are sharing 2 vCPU and 8 GB with a production site. A runaway build or a memory leak on our side could degrade theirs. Acceptable while next2 is paused; it is a reason to move production onto its own VM rather than grow into this one.

Archive is a first-class store, not a folder

Access points hold documents in transit then delete them; retention sits on the taxpayer. For every submitted document we keep the canonical JSON, the wire payload, the raw response, the generated UBL, the tax document, the message-level status and the rendered PDF — write-once, with an integrity hash, addressable for fifteen years.

Section 04

Repository

One new private repository: axb0234/invoicenext. Plus a small second one for the shared code, because vendoring it into two products invites drift.

invoicenext/
  api/
    app/
      core/        # tenancy, RLS enrolment, session, config — written fresh for Postgres
      authz/       # function-roles + scope; catalogue shape kept, keys rewritten
      ar/          # customers, items, quotes, invoices, credit + debit notes, receipts
      ap/          # suppliers, inbound inbox, bill registration, acknowledge / dispute
      einv/        # canonical model, validation, enrichment, state machine, adapters
      gl/          # tier two — vouchers, periods, trial balance, statements
      print/       # WeasyPrint doctype registry + bilingual templates
      imp/         # CSV / Excel import framework (lifted)
      jobs/        # Postgres queue + workers
      billing/     # plans, metering, self-serve signup — nothing to lift, all new
    alembic/versions/
    tests/
  web/
    src/
      shell/       # nav, ⌘K catalogue, IA budget test
      ui/          # primitives on the design-kit tokens
      locales/     # en.json · ar.json · pseudo.json
      ar/ ap/ einv/ gl/ billing/
    e2e/           # Playwright, incl. an RTL pass
  infra/           # Caddyfile · docker-compose.yml · deploy.sh
  planning/        # BUILD_JOURNAL.md · PARALLEL_PROTOCOL.md · GAP_MATRIX.md
  vendor/fontawesome-pro/   # licensed, self-hosted, not from a CDN

The shared package

axb0234/aspirtek-kernel — pure Python, no schema, no migrations. Money and currency precision, the import framework core, the approvals effects registry, grid and export. Consumed by pinned tag:

pip install "aspirtek-kernel @ git+ssh://git@github.com/axb0234/aspirtek-kernel@v1.0.0"

A conformance test in each product's CI asserts the Python and TypeScript money implementations agree — the same technique next2 already uses to keep money.py and currency.ts in step. No cross-repository migration link ever exists.

Branching

main is always deployable. Mission branches mission/<tag> in worktrees on /data/inx-wt/<TAG> — never under ~/projects, which is the session host's working directory. Every mission gets its own database inx_<tag> and its own virtualenv, because an editable install resolves to the checkout it came from and a shared venv silently tests the wrong code. Never git stash in a fleet — the stash stack is shared across worktrees.

Section 05

Pipeline

JobRunnerDoesTarget
api-testsGitHub-hostedruff, pytest with an ephemeral Postgres service container, coverage floor< 8 min
web-testsGitHub-hostedtsc, vitest, i18n extraction check, IA budget test< 4 min
deployself-hosted invoicenextfetch, build, migrate, up, health-poll, prune images and builder< 3 min
integrationself-hostedreal DB, RLS isolation as a non-superuser, ASP sandbox round trip< 10 min
e2eself-hostedPlaywright: sign-up, issue, submit, resolve, acknowledge — LTR and RTL< 8 min
Two CI traps inherited from next2 — designed out, not documented around

Integration jobs must not skip silently. On next2 a red unit suite buys a free pass for every integration test behind it, and a missing connection string made the catalogue tests assert against an empty result rather than fail. Every environment assertion here is paired with a positive count, so an invisible catalogue fails loudly instead of passing quietly.

Read CI at the job level, never the run rollup. The run conclusion settles before the post-deploy jobs finish. And a cancelled run means superseded by a newer push, not failure — read green off the last run that actually completed.

Section 06

Environments

next2 has none beyond production. We need one more, and it is not optional: a sandbox wired to the provider's sandbox, because the alternative is testing tax submissions against the real authority.

EnvironmentDatabaseProviderPurpose
local — this VMDocker Postgres on /dataMock adapterDay-to-day. The mock is the only Taxilla path that exists at all
CIService container, from empty each runMockProves migrations apply from empty, not just from yesterday's shape
staging — next2appContainer, own volumeSMARTeIS stagingapp.invoicenext.ai. Real round trips, demo environment, where the pilot starts. Archives to dev/
productionOwn VM, provisioned at launchLive, per-tenant credentialsKill switch, staged OFF → MANUAL → AUTO. Archives to prod/

The staged rollout is carried straight from the OIG build, where it worked: a tenant starts OFF, moves to MANUAL where a human presses submit, and only then to AUTO — and switching to AUTO requires typing the company code to confirm.

Section 07

Phases

Sixteen weeks to a sellable Oman product with a customer pilot running. The ledger tier follows; it is a commercial decision, not a compliance one, and the Oman deadline for our band is 1 October 2027.

Week 0now

Ground

Owner-led, no engineering capacity. Detailed in section 10.

  • Domains, VM, repository, ISO scope decision, provider procurement.
Weeks 1–3P1

Spine

  • Kernel: tenancy, Postgres RLS, session, authz, audit trail, numbering.
  • Self-serve signup and the plan/metering model — nothing to lift, and it is the commercial layer.
  • Shell, design tokens, i18n scaffolding with pseudo-locale failing the build.
  • CI, deploy, sandbox environment live.
Weeks 3–6P2

Receivables and the universal on-ramp

  • Customers, items, invoices, credit and debit notes, receipts with FIFO allocation.
  • Bilingual document rendering, with isolate injection in the data layer from the first commit.
  • CSV / Excel import with a real validator; public REST API with keys and a sandbox.
Weeks 5–9P3

Compliance engine, both directions

  • Port einv from next2; Python lifts, SQL is rewritten for Postgres.
  • The enrichment layer — the missing half, and the best idea in the OIG build.
  • SMARTeIS adapter completed, four known defects fixed; Complyance built against its free sandbox as the abstraction's stress test.
  • Inbound rails: inbox, supplier matching, bill registration, acknowledge and dispute as wire actions.
  • QR constants verified against the OTA specification. Currently best-guess.
Weeks 8–13P4

Depth and Arabic

  • Quotes, progress invoicing, retainers, recurring, expenses, customer portal, reminders.
  • Full Arabic interface; RTL regression pass in Playwright.
  • The settings surface — budgeted honestly at 40% of the build.
  • Reports: registers, ageing, statements, VAT working paper, Checklist annexures.
Weeks 12–16P5

Connectors and pilot

  • On-prem agent and Tally as one project — Tally forces the agent to be genuinely general.
  • Cloud tier: Zoho Books, QuickBooks Online, Xero, Odoo.
  • Oman pilot with a real customer through SMARTeIS.
  • Marketing site, published pricing, scope-and-deadline checker.
Latertier 2

Ledger

Vouchers, periods, trial balance, IFRS statements, bilingual printed accounts. Sold to customers we already have, whose data is already in the system, against a competitor whose customers face a migration to leave.

Section 08

Mission board — phases 1 and 2

Disjoint ownership, each on its own worktree and database, merged serially through one coordinator. A trap one mission pays to find is handed to the next for free.

M-KERN-1
Tenancy and security kernelPostgres RLS, SET LOCAL per transaction, session, password flow with the change-password gate inside the session dependency so a new router cannot forget it
M-AUTHZ-1
Function-roles, scope, auditCatalogue shape kept from next2, keys rewritten — roughly 30, not 522
M-SIGN-1
Self-serve signup, plans, meteringThe entire commercial layer. No precedent to lift
M-SHELL-1
Shell, tokens, i18n, IA budget testDesign kit into components; pseudo-locale fails the build on an unextracted string
M-AR-1
Customers, items, invoicesItem master does not exist in next2 — built from zero
M-AR-2
Credit and debit notes, receipts, allocationSettlement logic ports from next2 and is better specified than most commercial products
M-DOC-1
Bilingual renderingWeasyPrint, paired labels, isolate injection in the data layer, Noto/Plex Arabic pinned as a build artifact
M-IMP-1
Import and public APIThe month-one on-ramp: four weeks buys 100% coverage where the first connector buys 30%
Sequencing constraint

M-KERN-1 lands before anything else merges. Every other mission enrols tables into RLS through its helper, and a second implementation appearing in parallel is how two products end up with two tenancy models. M-SHELL-1 and M-DOC-1 can start immediately against the design kit.

Section 09

Verification

Eleven rules from next2, each learned by something breaking. Restated for this stack, with three added for what is new here.

#Rule
V1An isolation assertion made as a superuser proves nothing — Postgres RLS is bypassed by BYPASSRLS and by the table owner. Run isolation checks as the application role.
V2After re-pointing a migration in a rebase, your database is stale and Alembic still reports head. Rebuild from empty.
V3Freeze the tree and the database before a gate run.
V4A missing connection string does not under-test, it lies. Pair every environment assertion with a positive count.
V5A present connection string masks a unit-test failure exactly as a missing one fakes a pass. Mirror CI honestly with env -u.
V6"CI green" requires the post-deploy jobs to have succeeded, not to have been skipped.
V7Never relax a security assertion to make a shared-environment failure go away.
V8Field length limits bound the destination column, not the source.
V9Dogfood finds what tests structurally cannot. Walk it as a customer.
V10Never rebase a branch a live mission still owns — a background agent can resume.
V11pytest … | tail returns tail's exit code. Capture the status straight off pytest. A truncated log ending in a success line is indistinguishable from a pass.
V12New. Row counts read through the application role with no tenant set return zero for everything — RLS filtering, not an empty database. Verify data as the owner.
V13New. An RTL bug is invisible in a screenshot of a correct-looking page. Assert glyph order, not appearance — <bdi> works in the browser and silently fails in WeasyPrint.
V14New. Never submit to a live authority from a test. Wrong-environment submission is a real filing, and one provider selects environment by a field in the payload on the production host.
Section 10

Week one

Ordered by whether a clock is running. The first two have external deadlines.

Buy invoicenext.ai and create the A recordapp.invoicenext.ai → 40.80.84.69. This is the long pole: Caddy cannot issue a certificate until the name resolves, so nothing can be deployed for you to look at until it exists. Register .io / .ae / .om at the same time.
Domain drop-catch on invoicenext.comRegistry expiry 1 September — roughly twenty days. Broker offer in parallel, since the annual-renewal pattern says they will probably renew.
Settle the ISO 27001 certificate holder, then signMulti-site covering both entities, or issued to the accreditation applicant with India as an in-scope location. Confirm with the certification body and the OTA before the engagement letter.
Provision the VM and the Flexible ServerSame region as next2app. Attach the self-hosted runner with the invoicenext label.
Create axb0234/invoicenext and aspirtek-kernelPrivate. Branch protection on main. Secret scanning on — the July leak is why.
Open the provider conversationsPass/fail: programmatic tenant provisioning, a per-tenant price floor, and ISO 27001 held or committed by October 2027. Nineteen questions are already itemised.
Written questions to the OTA and the UAE Ministry of FinanceTop of the list: reconcile the two Oman rollout schedules our research produced. Also Taxilla's accreditation reference, and the VAT return box schema.
Confirm the corner-1 reading with UAE tax counselOur reading of the operative texts is that creating an invoice is not a regulated activity. The ministry has never published those words.
Launch M-KERN-1Everything else waits on the tenancy helper.
Two items that belong to next2, not here

The SMARTeIS adapter running in JobNext production has four defects against the v2.1 specification — a wrong inbound path, a malformed acknowledgement call, and an inbound mapper using outbound field names that would return empty records. That is the OIG pilot. Separately, next2 is parked mid-programme with a ranked open queue. Neither should be quietly absorbed into this plan; both need their own decision.

Section 11

Cost

ItemMonthlyOne-offNote
Staging server$0next2app, already paid for — next2 is paused
Development machine$0This VM, already paid for
PostgreSQL$0Container. Managed service deferred until scale demands it
Blob archive~$5Provisioned. Grows with document volume
Production VM~$35Not yet — provisioned at launch
Domains~$15$100–300Five TLDs; .ai carries the cost
Domain acquisition?Broker offer on the .com — unknown until asked
ISO 27001$28–55kVia India. Surveillance audits annually thereafter
Provider fees?The unresolved number. Published list is $1.10 per document; the structure matters roughly twelve times more than the rate
Font Awesome ProheldExisting licence; assets copied from production

Infrastructure to get to a testable staging product is about $5 a month — the blob, and nothing else. Everything until launch runs on hardware already being paid for. The two numbers that matter are the ISO certification, which is a real commitment on an external clock, and the provider's wholesale rate, which decides whether the pricing model works at all and cannot be discovered without a conversation.