Every screen, every table, every monthly line item — and a working demo you can click through, upload a spreadsheet into, and export a PDF out of, right now.
Across 11 modules. 43 of them are the phase-1 MVP.
Real state, real validation, real file import and export.
One VPS running app, database and queue. Scales to ~$640 at 100+ users.
Per seat, rising to $972 after year one. Yours is flat.
Your Storylane link — app.storylane.io/share/lddvudombcyf — is blocked by the network policy on the machine I'm running on, so I could not watch the demo and copy its exact screens. Everything below is built to the general shape of an SME ERP that handles clients and inventory, which is what you described. If you send me a screen recording, a PDF export of the Storylane, or just a list of its tab names, I will re-cut the screen inventory to match it exactly — the structure below is designed to be edited, not rewritten.
| Your question | The short answer |
|---|---|
| What's the full IT stack? | Next.js + TypeScript on the front, Node/tRPC API, PostgreSQL as the single database, Redis for queues, S3-compatible object storage for documents. Details in the stack section. |
| How many screens? | 91 across 11 modules. 43 are phase 1 (weeks 1–8), 37 are phase 2, 11 are phase 3. Full list here. |
| What's the data behind the screens? | About 45 tables. The field-by-field spec for the six screens that matter most is in the data model section; the rest follow the same pattern. |
| Can I get a mock-up of the whole thing? | Better than a mock-up — a working demo. Click any row, edit a record, drop a spreadsheet in, download a PDF. It keeps state. |
| Upload PDF, Excel and CSV? | Yes, built in — and working in the demo today. Map → validate → review errors → commit. How it works. |
| Download the data too? | CSV, Excel, PDF and JSON from every screen, plus scheduled exports that email themselves. Masked fields never reach the file. |
| Security features? | MFA, role-based access enforced in the database (not the UI), field-level masking, an immutable audit trail, encryption at rest and in transit, tested backups. Fourteen controls listed. |
| What database, and where do I host it? | PostgreSQL. Start on Supabase or a single Hetzner VPS; move to AWS me-central-1 (UAE) if you need data residency. Three options compared. |
| Total monthly cost? | ~$25/mo lean, ~$135/mo at 10–30 users, ~$640/mo at 100+ users and three entities. Flat, not per seat. Full breakdown. |
| Can Claude Code actually build this? | Yes — the demo you're about to open was built by it. The code is the easy part; the accounting rules and the data migration are the hard parts. Honest assessment. |
Nothing exotic. Every choice below is boring, widely hired for, and something you can walk away from — which matters more than performance when the thing has to run your company for ten years.
| Layer | Choice | Why this one |
|---|---|---|
| Front end | Next.js 15 (App Router) · React 19 · TypeScript | Server components keep the big tables fast; one language front to back; the largest hiring pool of any framework. |
| UI kit | Tailwind CSS · shadcn/ui · TanStack Table & Query | You own the component code rather than renting a design system. TanStack Table handles 50k-row grids with sorting, filtering and virtualisation. |
| Charts | Recharts, or hand-rolled SVG | The demo's charts are hand-rolled SVG — zero dependencies, works offline. Recharts once the dashboards get complicated. |
| API | tRPC (typed) + REST for outside callers | tRPC gives you compile-time errors instead of runtime 500s when a field is renamed. A thin REST surface stays for integrations. |
| ORM / schema | Prisma or Drizzle + versioned SQL migrations | The schema is the contract. Migrations are reviewed like code, run in CI, and never applied by hand to production. |
| Database | PostgreSQL 16 | Real transactions (non-negotiable for double entry), row-level security, JSONB for the fields every business wants and no one predicted, full-text search built in. More on why not the alternatives. |
| Cache & queue | Redis + BullMQ (or Inngest if you'd rather not run it) | Imports, PDF generation, emails and scheduled reports all run off the request cycle so a 12,000-row spreadsheet never times out a browser. |
| File storage | S3-compatible — Cloudflare R2 or Supabase Storage | Invoices, receipts, passports and trade licences live in object storage with expiring signed URLs, never in the database. |
| Auth | Supabase Auth, Clerk or Auth.js — TOTP MFA, SSO-ready | Never build session handling yourself. Google Workspace SSO the day you have more than ten staff. |
| Search | Postgres full-text + pg_trgm; Meilisearch later | Postgres covers you to roughly a million rows. Add a search engine when it actually hurts, not before. |
| File parsing | PapaParse (CSV) · SheetJS/ExcelJS (XLSX) · pdf-parse | Server-side and streamed, so memory stays flat regardless of file size. |
| Document AI | Claude API or AWS Textract for scanned PDFs | Only for documents with no text layer. Returns confidence per field; anything under the gate stops for a human. |
| PDF output | Puppeteer (server) or React-PDF | Server-side so an invoice looks identical whether it was emailed by a cron job or printed from a phone. |
| Email / SMS | Resend or AWS SES · Twilio · WhatsApp Business API | Invoice delivery, statement runs, approval requests, expiry alerts. |
| Observability | Sentry · Better Stack or Axiom · Vercel Analytics | You want to hear about the failed GRN post from a pager, not from your storekeeper three days later. |
| CI/CD | GitHub Actions — typecheck, lint, tests, migration dry-run | Nothing reaches production without the migration having been replayed against a copy of production first. |
| Testing | Vitest (unit) · Playwright (end-to-end) | Accounting logic gets unit tests with known-answer fixtures. The critical click-paths get Playwright. |
| Edge / WAF | Cloudflare — DNS, WAF, rate limiting, DDoS | Free at your volume and it sits in front of everything. |
Validation lives in exactly one place, next to the schema. A value typed into a form, pasted in from Excel, or pushed through the API all pass through the same rules. The moment you have two copies of "what a valid invoice looks like", your data starts drifting — and every ERP that has ever gone bad, went bad exactly there.
P1 · weeks 1–8 · 43 screensP2 · weeks 9–16 · 37 screens
P3 · later · 11 screens
Phase 1 is deliberately the smallest thing that can run the business end to end: take an order, hold stock, buy
more, invoice it, get paid, see the number. Everything else waits.
Forty of those eighty-four are the same screen with different columns — a list, a filter bar, a detail drawer, a form. Build that pattern once, generate the rest from the schema, and the 91 collapses to roughly 15 genuinely distinct pieces of work. The screens that actually cost you time are the invoice builder, the GRN posting, the payroll run, the import wizard and the VAT schedule. Budget accordingly.
Roughly 45 tables. Below are the six that everything else hangs off, with the exact columns, types and rules. The remaining tables follow the same shape: an id, a tenant, the business fields, and the four audit columns every row carries.
| Column | Type | Rule | Notes |
|---|---|---|---|
id | uuid | primary key | Never the display code. Codes get reused; ids never do. |
code | text | unique per tenant | What humans say out loud — CL-1001. |
name | text | required, unique per tenant | The registered legal name, not the trading name. |
trn | text | 15 digits, starts with 1 | Prints on the tax invoice. Wrong TRN = rejected input claim for your customer. |
segment, emirate | enum | from a lookup table | Lookups, not free text — otherwise you get "Dubai", "DUBAI" and "dxb". |
payment_terms | enum | Advance · Net 15/30/45/60 | Drives the due date on every invoice automatically. |
credit_limit | numeric(14,2) | ≥ 0 | Zero means cash only. Enforced at order entry, not discovered at month end. |
balance | numeric(14,2) | derived | Never stored as a typed number — computed from the ledger, or kept in a materialised view. |
status | enum | Active · Prospect · On Hold · Dormant | "On Hold" blocks new orders at the API, not with a warning dialog. |
owner_user_id | uuid → user | nullable | Drives the row-level security rule for the Sales role. |
created_at · created_by · updated_at · updated_by | timestamptz / uuid | set by trigger | On every table. No exceptions. |
| Column | Type | Rule | Notes |
|---|---|---|---|
item.sku | text | unique per tenant | The natural key. Imports match on this, which is what makes a re-import safe. |
item.barcode | text | unique, nullable | Nullable and unique — Postgres allows many NULLs, one of each value. |
item.uom | enum | PCS · KG · LTR · MTR · BOX | Changing the UoM on a live item is forbidden; you clone the item instead. |
item.cost | numeric(14,4) | ≥ 0, restricted | Four decimals — rounding at two will drift on high-volume, low-value stock. Hidden from the Sales role. |
item.price | numeric(14,2) | ≥ 0 | Hidden from the Warehouse role. |
item.reorder_level | integer | ≥ 0 | Feeds the reorder screen and the nightly alert. |
stock_level | (item, warehouse, bin) → qty | composite unique | Quantity lives per location, never on the item. The item's "on hand" is a sum. |
stock_move | append-only | no UPDATE, no DELETE | Every in and out, with its source document. This is the table an auditor reads. |
stock_move.qty_delta | numeric(14,4) | ≠ 0 | Signed. A transfer is two rows that net to zero, written in one transaction. |
Storing qty_on_hand as an editable column on the item, and updating it from five different places. Six months later the number is wrong and nobody can prove when it went wrong. Stock is a ledger: you append movements, and the balance is derived. It is slightly more work on day one and it saves the entire system on day four hundred.
| Column | Type | Rule | Notes |
|---|---|---|---|
number | text | unique, gapless | Allocated by a database sequence inside the transaction. A cancelled invoice is voided, never deleted and never reissued. |
client_id | uuid → client | required | Restrict on delete. A client with invoices can never be removed, only archived. |
issue_date · due_date | date | due ≥ issue | Due date defaults from the client's payment terms. |
currency · fx_rate | char(3) / numeric | rate frozen at issue | Freeze the rate on the document. Re-deriving it later silently rewrites your history. |
line.qty · unit_price · discount_pct | numeric | qty > 0 | Per line, not per invoice. |
line.tax_rate · tax_amount | numeric | 5% UAE standard | VAT is computed per line and then summed. Computing it on the invoice total gives you rounding differences that the FTA will notice. |
subtotal · tax_total · grand_total | numeric(14,2) | derived, checked | Stored for speed, but a constraint asserts they still equal the sum of lines. |
amount_paid · balance | numeric(14,2) | derived from receipts | A receipt can be split across several invoices — that's a join table, not a column. |
status | enum | Draft→Sent→Part Paid→Paid / Overdue / Void | A posted invoice is immutable. Corrections are credit notes. |
| Table | Writes to | The rule that matters |
|---|---|---|
purchase_order + lines | nothing financial | A PO is a promise, not a transaction. It moves no stock and books no cost. |
goods_receipt + lines | stock_move + GRNI accrual | The moment stock becomes yours. One transaction writes the movement and the accrual, or neither. |
bill (supplier invoice) | accounts_payable, reverses GRNI | Three-way match: PO price × GRN quantity × bill total. Any mismatch above tolerance stops for a human. |
supplier_payment | bank + AP | Cannot be raised against an unmatched bill. That single constraint is most of your fraud control. |
| Column | Handling | Why |
|---|---|---|
emirates_id · passport_no | encrypted at column level | Personal identifiers under the UAE PDPL. Encrypted with a key held outside the database, so a stolen dump is useless. |
iban | encrypted, masked in UI | Shows as AE•• •••• 4471 everywhere except the payroll run itself. |
basic_salary · allowances | row-level security | Visible to Owner, Finance and HR. The API never sends it to anyone else, so it cannot leak through an export. |
visa_expiry · passport_expiry · labour_card_expiry | indexed, alerted | A nightly job raises alerts at 90, 60, 30 and 0 days. This one field pays for the system on its own. |
person_id · employer_id | required for WPS | The bank's SIF file rejects the whole batch if one is malformed. Validate on entry, not on payday. |
| Column | Type | Notes |
|---|---|---|
occurred_at | timestamptz | Database clock, not the browser's. |
actor_user_id · actor_role | uuid / enum | The role in force at the time, not the role they hold today. |
action | text | INVOICE.VOID, ITEM.PRICE_UPDATE, EXPORT.CSV … |
entity_table · entity_id | text / uuid | Points at the row that changed. |
before · after | jsonb | The changed fields only, not the whole row. Enough to reconstruct, small enough to keep for seven years. |
ip · user_agent · request_id | inet / text | Ties a change to a session and a support ticket. |
Rows are written by a database trigger inside the same transaction as the change they describe — so a change cannot exist without its audit row. The application's database role is granted INSERT only: no UPDATE, no DELETE, not even for the owner's account. Doctoring the log requires superuser access to the database itself, which is a different and much louder kind of event.
This is the feature that decides whether the ERP gets adopted or quietly abandoned. If your team cannot get their existing spreadsheets in on day one, they will keep the spreadsheets — and then you have two systems instead of one.
CSV with any delimiter (comma, semicolon, tab, pipe — sniffed automatically), .xlsx, .xls, or a PDF. Quoted fields, embedded line breaks, Excel's BOM and Arabic text are all handled. Files stream to storage; nothing is held in memory.
"TRN Number", "Tax Reg No" and "VAT NUMBER" all resolve to the same field, with a confidence score shown beside each match. You only correct the ones it got wrong. In the demo, a seven-column Excel file with headers like PURCHASE_PRICE and MIN_QTY maps 7 out of 7 with no help.
Required fields, number formats, date parsing, enum membership, duplicate natural keys within the file, and duplicates against what is already in the database. Failures go to an error queue with the file line number and a plain-English reason — "Cost Price is not a number (“NOT-A-PRICE”) on line 5" — downloadable as a CSV you can hand back to whoever produced the file.
Good rows are written together or not at all. Each row stores a hash of its source line, so re-running the same file is a no-op instead of a duplicate — which is the single most common way an import destroys an inventory. The commit writes one IMPORT.COMMIT row to the audit trail.
A spreadsheet is structured; a PDF is a picture of structure. Two cases:
| Kind of PDF | How it's read | What you get |
|---|---|---|
| Has a text layer Exported from any accounting system |
Text extracted directly, then fields matched by pattern | Supplier name, invoice number, date, TRN, currency, VAT and grand total — plus the line items — each with a confidence score. The demo does this in your browser, live. |
| A scan or a photo Someone photographed a paper receipt |
OCR server-side — AWS Textract or the Claude API | Same shape of result, same confidence scores, costing roughly 1–4 US cents per page. |
Extraction is probabilistic. Your ledger is not. Fields above the confidence gate (85% is a sensible starting point) are pre-filled; everything else stops for a human. The record is created as a draft and still has to pass the three-way match against the purchase order and the goods receipt before anyone can pay it. Skip that gate to save thirty seconds and you will eventually pay a supplier twice.
| Format | Produced by | Use it for |
|---|---|---|
| CSV | Streamed, UTF-8 with BOM | Anything that will be re-imported somewhere else. Opens correctly in Excel including Arabic. |
| Excel | ExcelJS server-side, typed cells | Handing a workbook to your accountant with numbers that are actually numbers, not text. |
| Puppeteer, server-side | Invoices, statements, the ageing report your bank asks for. Identical output whether emailed by a cron job or printed from a phone. | |
| JSON | REST endpoint | Feeding a dashboard, a Power BI file, or another system. |
| Scheduled | Queue + email | AR ageing to you every Monday 07:00. Stock valuation to the auditor on the 1st. Nobody has to remember. |
When the Sales role exports the item list, the cost column is not hidden in the spreadsheet — it was never selected from the database. Same for salaries, IBANs and Emirates IDs. Verify it yourself: open the demo, switch the role to Sales Executive, open Item Master, export the CSV, and look at the header row.
| # | Control | Priority | What it actually means |
|---|---|---|---|
| 1 | MFA on every write role | Day one | TOTP app codes. A stolen password alone gets nobody in. Read-only roles can be exempt; anyone who can post a payment cannot. |
| 2 | Role-based access in the database | Day one | Postgres row-level security. The Sales role's query for other reps' clients returns zero rows — there is no endpoint to forget to protect. |
| 3 | Field-level masking | Day one | Cost prices, salaries, IBANs, Emirates IDs. Enforced in the query, so the UI, the API and every export agree. |
| 4 | Immutable audit trail | Day one | Written by trigger, insert-only grant, seven-year retention. Who, what, when, from where, before and after. |
| 5 | Encryption in transit and at rest | Day one | TLS 1.3 everywhere, AES-256 volume encryption, HSTS, secure cookies. |
| 6 | Column encryption for identifiers | Day one | Emirates ID, passport, IBAN encrypted with a key held in a KMS outside the database. A leaked backup is then just noise. |
| 7 | Tested backups | Day one | Daily snapshot plus point-in-time recovery, stored in a different region. An automated monthly restore that alerts on failure — an untested backup is a rumour. |
| 8 | Maker-checker approvals | Week 6 | Supplier payments, price changes, credit notes, stock write-offs and user role changes need a second person. This is where internal fraud actually happens. |
| 9 | Session policy | Week 4 | 30-minute idle timeout, visible device list, forced re-authentication before payment actions. |
| 10 | Rate limiting & lockout | Week 4 | Five failed sign-ins locks the account for fifteen minutes and alerts the owner. Per-IP limits on the API. |
| 11 | Signed, expiring document URLs | Week 4 | A link to a passport scan works for five minutes for one authenticated user. Object storage is never public. |
| 12 | Export logging & thresholds | Week 8 | Every export names the user, dataset, row count and columns. Bulk exports above a threshold can require a second approver. |
| 13 | Dependency & secret scanning | Day one | Dependabot, secret scanning in CI, a vault for credentials. No .env in the repository, ever. |
| 14 | Penetration test before go-live | Before cutover | One external review, roughly $3,000–8,000 in the UAE. Cheaper than the first breach by several orders of magnitude. |
The federal PDPL (Decree-Law 45 of 2021) governs personal data — your employees' documents far more than your stock records. There is no blanket residency mandate for a private trading company, but regulated sectors (healthcare, finance, some government contracts) and DIFC/ADGM entities have their own rules. If you are unsure, or you expect to bid for government work, host in me-central-1 (AWS UAE) or Azure UAE North and the question stops being a question. Confirm your specific obligations with a UAE lawyer before go-live — I am describing the general landscape, not giving you legal advice.
| Option | Verdict | Reasoning |
|---|---|---|
| PostgreSQL | Use this | Real ACID transactions — mandatory the moment you have double entry. Row-level security is the cleanest RBAC you will find. JSONB absorbs the custom fields every business turns out to need. Full-text search included. Runs anywhere, and every developer you will ever hire knows it. |
| MySQL / MariaDB | Workable | Perfectly capable, but weaker JSON handling and no row-level security, so permissions move back into application code where they are easier to get wrong. |
| MongoDB | Wrong tool | An ERP is relentlessly relational — an invoice line references an item references a supplier. Enforcing that by hand in application code is a decade of bugs you have volunteered for. |
| Airtable / Google Sheets | Fine to start, fatal to stay | No transactions, no real permissions, no audit trail, and it degrades badly past a few tens of thousands of rows. Excellent for validating the idea; it cannot be the system of record. |
| SQLite | Only for the demo | Superb single-user database. Concurrent writers from a warehouse and an accounts desk is not its job. |
Hetzner CPX31, Docker, Coolify. App, Postgres, Redis and backups on one box you control completely. Cheapest by a wide margin and genuinely fine for a single company under about 20 users.
Trade-off: you are the sysadmin. Patching, monitoring and the restore drill are yours. Budget two hours a month, and know that a bad night is a long night.
~$25/moEU regions
Vercel + Supabase (or Neon) + Cloudflare R2. Postgres, authentication, storage, row-level security and point-in-time recovery arrive configured. You write features instead of Terraform.
Trade-off: costs more, and you inherit someone else's outage. Both are still plain Postgres underneath, so leaving is a pg_dump, not a rewrite.
~$135/moFrankfurt / Singapore
AWS me-central-1 or Azure UAE North. RDS Postgres multi-AZ, ECS or App Runner, S3, KMS. The answer when data residency is a contractual requirement or you are selling to government.
Trade-off: roughly four to five times option B, and it needs someone who genuinely knows AWS. Do not pick this one for comfort — pick it because a contract made you.
~$640/mo+UAE residency
Start on B. It gets you to a working system fastest, and every part of it is standard Postgres, so nothing you build gets stranded. Move to C the day a customer contract or a regulator asks for UAE residency — that migration is a dump, a restore and a DNS change, roughly a weekend. Choose A only if the monthly cost genuinely matters more than your weekends.
List prices as published in August 2026, in US dollars. Providers change these — treat them as accurate to about ±15% and re-check at signup. Everything below excludes VAT.
| Line item | $ / month | Note |
|---|---|---|
| Hetzner CPX31 VPS — 4 vCPU / 8 GB, running app + Postgres + Redis | 18 | Rising to ~25 on the announced 2026 increase |
| Off-site backup storage (1 TB Storage Box) | 4 | Nightly encrypted dumps |
| Cloudflare R2 object storage — ~100 GB of documents | 2 | No egress charges |
| Domain + Cloudflare DNS & WAF | 1 | WAF is free at this volume |
| Transactional email (Resend free tier, 3k/mo) | 0 | $20 above 50k |
| Sentry & uptime monitoring (free tiers) | 0 | |
| Total | ~25 | ≈ AED 92 / month |
| Line item | $ / month | Note |
|---|---|---|
| Vercel Pro — 1 seat | 20 | Includes 10M edge requests; overage $2/M |
| Supabase Pro — 8 GB database, 100 GB storage, 100k MAU | 25 | Includes a $10 compute credit |
| Supabase compute upgrade to Small | 5 | Difference above the included Micro |
| Cloudflare R2 — ~500 GB | 8 | |
| Upstash Redis — queues, cache, rate limits | 10 | Pay-per-request |
| Resend — 50k emails | 20 | Invoices, statements, alerts |
| Sentry Team | 26 | |
| Background jobs (Inngest, or self-hosted BullMQ at $0) | 20 | |
| Total | ~134 | ≈ AED 492 / month |
| Optional: Supabase 7-day point-in-time recovery | +100 | Worth it once the ledger is real money |
| Optional: PDF/OCR extraction, ~2,000 documents | +30–80 | Only for scans with no text layer |
| Line item | $ / month | Note |
|---|---|---|
| Application hosting + seats + usage | 80 | |
| Managed Postgres, multi-AZ with read replica | 250 | RDS me-central-1 or Neon Scale |
| Managed Redis | 40 | |
| Object storage ~2 TB + egress | 35 | |
| Email, SMS and WhatsApp Business | 60 | |
| Observability — errors, logs, traces | 90 | |
| Disaster recovery — second region, PITR | 60 | |
| Cloudflare Business — WAF, bot management | 25 | |
| Total | ~640 | ≈ AED 2,350 / month |
| Option | $ / month | $ / year | What you get, and what you don't |
|---|---|---|---|
| Your own, Tier B | 134 | 1,608 | Flat. Adding a 26th user costs nothing. You own the code and the data outright. |
| Odoo Standard | 778 | 9,330 | $31.10/user first year, then $38.90 — so year two is $11,670. Mature and huge; you rent it forever, and customisation means an Odoo partner. |
| Odoo Custom | 1,525 | 18,300 | $61/user first year, then $76.20. Adds Studio, multi-company and API access. |
| Zoho One | 925 | 11,100 | $37 per employee per month on the all-employee plan — note that is every employee, not every user. |
| SAP Business One (Professional) | 2,275 | 27,300 | $91/user/month, plus implementation typically $20k–80k. Genuine enterprise depth you will not use. |
| Oracle NetSuite | 3,474 | 41,690 | ~$999 base + ~$99/user, plus a six-figure implementation. Wrong shape at your size. |
Commercial ERP is priced per seat; your own is priced per server. At five users the packaged products are cheaper and you should buy one. At twenty-five users you are paying roughly $9,000 a year to rent software you cannot change. At a hundred users you are paying $37,000 a year — and by then the build has paid for itself several times over and you own the asset.
| How you build it | Phase 1 (34 screens) | Elapsed | Honest read |
|---|---|---|---|
| You + Claude Code | $100–200/mo subscription + your time | 10–16 weeks part-time | Real, and this page is the evidence — but you personally have to review the accounting logic, and you carry the risk. |
| One developer + Claude Code ← best value | $10,000–25,000 | 8–12 weeks | Roughly 250–450 hours of human time, mostly review, integration and migration. This is where the economics genuinely work. |
| Offshore dev shop, no AI | $27,000–84,000 | 4–7 months | 900–1,400 hours at $30–60. Quality varies enormously. |
| UAE agency | $60,000–150,000 | 5–9 months | You are buying accountability and a local phone number. |
Add, on top of any of those: a one-off penetration test ($3,000–8,000), an accountant to sign off the chart of accounts and VAT treatment ($1,000–3,000), and — the item everyone forgets — data migration from whatever you run today. Budget two to four weeks for the migration alone, and run both systems in parallel for one full month before you switch anything off.
The demo linked from this page was built by Claude Code in a single session: the schema, all 29 working screens, the CSV parser, the XLSX unzipper, the PDF text extractor, the role engine, the export pipeline and the browser tests that verify them. So the question is not whether it can write the code. It is which parts you must not delegate.
✓ Schema design and migrations
✓ Every list / detail / form screen
✓ Import parsing, mapping and validation
✓ Export in all four formats
✓ Role and permission plumbing
✓ Standard reports and dashboards
✓ Test suites and CI pipelines
✓ The boring 70% that makes a shop quote six figures
! Double-entry correctness — an accountant signs the chart of accounts, not a model
! VAT edge cases — reverse charge, designated zones, blocked input tax
! WPS SIF format — the bank rejects the batch, not the row
! Concurrency on stock — two people picking the last unit needs a real lock
! Migrations against live data — rehearsed on a copy, always
! Your data migration — nobody else knows what your legacy fields mean
! The security review — an outside pair of eyes before go-live
| Week | Build | Done means |
|---|---|---|
| 1 | Schema, auth, roles, app shell | You can sign in as three roles and see three different navigations. |
| 2–3 | Clients, Items, Suppliers + the import wizard | Your real master data is in, imported from your existing spreadsheets. |
| 4–5 | Purchase order → GRN → stock ledger | A real shipment is received and the stock number is correct and provable. |
| 6–7 | Quotation → invoice → receipt → AR ageing | You raise a real VAT invoice and record a real payment against it. |
| 8 | Reports, export centre, PDF pipeline | Phase 1 complete — the business could run on it. |
| 9 | Expenses, chart of accounts, journal | Every module posts to the ledger; the trial balance nets to zero. |
| 10 | VAT schedule + accountant review | Your accountant confirms the quarter's numbers against their own working. |
| 11 | Employees, payroll, WPS export | The bank accepts a test SIF file. |
| 12 | Projects and job costing | You can see which jobs lost money. |
| 13 | Approvals, audit hardening, backup drill | You restore the database from a backup and it works. |
| 14 | Penetration test and remediation | Findings closed, retested. |
| 15 | Data migration + parallel run begins | Both systems produce the same closing balances. |
| 16 | Training, cutover | The old system is read-only, not deleted. Keep it for a year. |
It is never the code. It is week 15. Every ERP project that goes wrong goes wrong at migration: the legacy stock quantities don't reconcile, three years of half-finished purchase orders have no clean equivalent, and somebody wants to go live anyway because the date was promised. Run in parallel for a full month. Both systems, same transactions, and compare the closing trial balance. If they disagree, do not cut over — no matter what the date says.
43 of the 91 screens are clickable, across six roles that genuinely change what you can see, with real file import and export. Drop a spreadsheet into it. Export a PDF. Switch to the Sales role and try to find the cost price.
Open the Tritek Marine Services ERP demo