One reusable connector for hundreds of independent NetSuite accounts.
One integration layer, many customer accounts, each with its own credentials, own schema, own quirks — and no per-customer code branch. This page is the evidence behind that sentence: what was built, what was measured, and exactly what is simulated.
Why this shape. An accounting platform only wins if it can onboard a firm's whole portfolio, not one client at a time. A firm is 50% QuickBooks, 20% Xero, 30% NetSuite, and every NetSuite account is different: different custom fields, different subsidiaries, different API permissions. So the connector had to be one engine, many accounts — tenancy as data, not as a deployment or a fork.
What that means in practice. Adding a customer is a row. Adding an ERP is one connector class plus one register() call. Nothing in the sync engine, retry layer or API knows which customer it is serving.
Read in five minutes: this block → the four questions → measured scale → what is simulated, and the 30/60/90. The videos are below if you want the console rather than the prose.
Your four questions, answered with evidence
These are the questions that decide whether this is the right experience. Each one points at something checkable on this page rather than at an adjective.
| Question | Answer | Where to check it |
|---|---|---|
| A single reusable connector across multiple independent NetSuite accounts, each with its own credentials and configuration? | Yes — that is the design, not a feature. Credentials, base URL, account id, API version, field names and transport choice are all data on a Connection row. Per-tenant credentials are sealed with HKDF-derived keys, and the demo proves the negative: copy one tenant's credential blob onto another and it fails to decrypt. Tests assert no plaintext credential can appear in any response, under load. | Demo §1 Provision and §2 Isolation (below). Load evidence: “no secret leaks, no cross-tenant rows” is reported as an invariant on every run. |
| Direct work with NetSuite APIs — SuiteTalk REST/SOAP, RESTlets, SuiteScript? | All four, behind one ErpConnector interface, plus SuiteQL. Batching behaves differently on each; the interesting part is proving they agree. A transport-equivalence contract test drives the same records through REST, SOAP and a RESTlet and asserts identical output — it found two bugs that no unit test could reach. |
§4 Customisation, §5 Conflict (below), and the Lessons Learnt video: “contract-test interchangeable transports, not each one in isolation”. |
| Recent hands-on Python: bidirectional sync, retries, idempotency, conflict resolution? | All four, in Python (FastAPI + SQLAlchemy + pytest). Bidirectional sync with per-page cursor persistence; idempotency keyed on business identity, not request id; retries with full jitter that honour Retry-After as a floor; conflicts detected at field level with a manual-resolution default for ledger data. 240 tests, ~30.4k lines. |
§3 Idempotency in the walkthrough; scale table for the test suite and runtimes. |
| Permanent hands-on engineering rather than consulting? | This is my own platform: deployed behind Apache on a VPS, with its own Prometheus scrape job, Grafana dashboard and systemd unit. Not a client deliverable, not a slide. | Appendix: stack. Dashboard and deployment are live at erpdashboard.sites.miomni.me. |
Evidence
Why “the demo reads the platform's own numbers” is the important sentence
It is the difference between a video and evidence. Every count comes from /metrics or the API, so an over-claim fails the run instead of flashing past. That discipline is what caught the dashboard reporting a circuit breaker as closed while it was open — a UI reading the wrong key, confidently wrong.
Measured scale
All numbers below were produced on this machine: an 8-core Apple laptop, the platform and the simulated NetSuite account running locally, Locust driving real HTTP through the real stack — simulator included, nothing mocked. They are laptop numbers, not data-centre numbers, and they are stated that way on purpose.
| Measurement | Result | How it was measured |
|---|---|---|
| Tenants provisioned and backfilled | 7 in 5.3 s | Full provision → paged backfill across 5 entities per account, through the real API surface |
| Read/dashboard load, 16 users · 90 s | 678 req · 0 failures | p50 16 ms, p95 270 ms; 6 connections syncing; p95 spread 3.93× |
| Sync + signed webhook ingest, 32 users · 120 s | 2,643 req · 0 failures | p50 14 ms, p95 310 ms; 1,281 signed webhook events (≈10.7/s); 100 sync runs |
| Mean sync run duration | 44–84 ms | From the platform's own sync_run_seconds histogram, both runs |
| Correctness under load | clean | Every response walked for plaintext credentials; every row checked against its owning tenant; instrumentation headers asserted |
| Test suite | 240 tests, ~60 s | 20 files, 0 failures, 12 skipped (simulator-only paths). pytest -q |
| Codebase | ~30.4k lines | Python + TypeScript/TSX across 115 files (dashboard included) |
The one number that is not clean, and what I think it means
At 32 users the harness's own fairness detector fired: p95 spread 5.02× across 7 connections (slowest 1,015 ms, fastest 202 ms). My reading is that this is the laptop, not the isolation: the simulator and 32 Locust users share the same 8 cores, so the account being asked for the most work drags. The honest next step is to re-run with the simulator pinned to its own machine before claiming per-connection isolation holds at that load — which is exactly the kind of question a real tenant would answer in week one.
What this means at the size the role is actually about
Extrapolation, with the assumptions stated so you can disagree with them: a firm with 300 client portfolios at ~1,500 invoices a year each is roughly 450k invoice movements a year — about 50 an hour, about 1 a minute, with bursts at close. The load above is already two orders of magnitude past that on a laptop. The work at that scale is not request throughput; it is onboarding (schema discovery per account), dedupe at import and export, and observability that tells you which of a thousand tenants is unhappy without opening a thousand dashboards.
What is simulated, and the 30/60/90 that closes it
Real
- TBA-signed SuiteTalk SOAP, SuiteTalk REST, SuiteQL, RESTlet calls
- SuiteScript 2.x running inside the account
- Paging, truncation and exhaustion semantics
- 429 +
Retry-After, and a throttle that pushes back - Error codes and their meanings, including auth rejection
- OneWorld multi-subsidiary accounts, custom fields and objects, drifted types
Simulated
- The customer's NetSuite account itself
- Its latency distribution and rate-limit profile
- Its data, including the deliberately hostile archetypes
- The faults, which are injected on purpose rather than waited for
Not yet proven on a live tenant
- Real throttle behaviour at sustained volume
- Schema drift over months of account changes
- Account-side performance at thousands of records
- The Oracle support loop when something is genuinely their bug
So here is how I would close that gap, and what “done” means at each step.
| Window | What happens | Exit criterion |
|---|---|---|
| Days 1–30 | Take the three worst archetype tenants from the customer base — the untrustworthy schema, the OneWorld multi-subsidiary, the restricted-permission account — and reproduce each in the harness. Prove dedupe and close-period replay against them. | All three sync, reconcile and replay cleanly; the failures I find are in the harness, not in production. |
| Days 31–60 | First live tenant behind a per-connection circuit breaker, with a written cutover and rollback. Dashboards wired to the tenant's real numbers, not the simulator's. | A week of live sync with zero duplicates and zero unreconciled periods; a breaker you have seen open and close on real throttling. |
| Days 61–90 | Tenants two and three, schema-drift watch, alert thresholds tuned to the noise floor we actually observe. | Onboarding a fourth tenant is configuration plus a canary run — no code change, no whiteboard. |
Ledger semantics — done, partial, not yet
The integration is only as good as its accounting. Marked honestly, because the gaps are the interesting part:
| Concept | State | Notes |
|---|---|---|
| Subsidiaries / OneWorld | Done | Mapped per account; a multi-subsidiary archetype is provisioned and synced in the demo. |
| Period locks and close | Done | Close-period replay is a first-class case in the idempotency tests; posting into a locked period fails loud rather than being retried. |
| Invoices, payments, credit memos | Done | Treated as first-class entities, not “invoices only”. A connector that syncs invoices but not payments always looks wrong to an auditor. |
| Reversal, not delete | Done | Removals are tombstones and reversals; nothing is hard-deleted in the ledger. |
| Custom fields and objects | Done | Discovered into a schema snapshot and treated as configuration — the field name the account actually has, or the ERP drops the data silently. |
| Tax codes and dimensions | Partial | Mapped when present; no policy yet for accounts whose tax model differs structurally. |
| Intercompany and eliminations | Partial | Multi-subsidiary works; elimination entries are not yet modelled. |
| Functional-currency conversion | Not yet | Next sprint: mapping currency and subsidiaryId is not the same as converting the way the account does — comparing two subsidiaries' amounts without it is how reconciliation arguments start. |
| Multi-line journals with line ordering | Not yet | A composite key one field short corrupts balances silently (missing lineSequenceNumber). On the list, with a failing test written first. |
How I work: the bugs the tests and the deployment caught
Twelve of them, written up as do/don't code. This is the part of the page I would want an engineer to read, because it is the part that cannot be faked.
- An upsert whose defaults are the state resets the state — a circuit breaker that could never open.
Retry-Afteris a floor, not a suggestion. Keep your own backoff separate from their instruction.- Contract-test interchangeable transports; testing each in isolation proves nothing about agreement.
- Write the field name the account actually has, or the ERP drops your data silently.
- A failing test is a hypothesis. Here the test was wrong:
internalIdis per-account. - A UI that reads the wrong key is confidently wrong. Report the worst state across keys.
- RFC 5849 signs form-encoded bodies only — a JSON body in the signature fails every write, and no read.
- A composite key one field short corrupts balances silently (missing
lineSequenceNumber). - A guard you only observe on a second run never fires: the cursor already moved.
- A running service never drops the schema it serves from. Reset rows, not tables, and lock the endpoint.
rate()andincrease()are hostile to churning labels. Use cumulative values there.- Never name a package after a stdlib module, and probe every meta endpoint, not the cheapest one.
Two more that are not in the twelve: deployment finds what the suite cannot — a broken readiness probe and a static bundle missing a file both only appear behind Apache.
Next sprint, in the open
- Functional-currency conversion and mapping for OneWorld, so subsidiary amounts compare the way the account means them.
- Safer dead-letter replay — a replay needs the same idempotency discipline as a normal write, plus an audit trail.
- A fourth ERP. NetSuite, Dynamics 365 and QuickBooks already sit behind the same interface; the next one is a class plus a
register()call, which is the point.
If we work together
What I would be judged on, and would judge myself on: the first live tenant. A platform that can onboard hundreds is only proven once the second and third go live without a code change — configuration, a canary run, and no drama.
What I would do first: reproduce your three worst accounts in the harness, prove dedupe and close-period replay against them, then take one live behind a breaker. That order is deliberate: the failures should happen where they are cheap.
The thing I would want you to know: my NetSuite work to date is in simulated accounts and a fault-injecting harness, not on live customer tenants. The mitigation is above, in weeks rather than quarters — and everything you are reading was built to make that gap obvious rather than hidden.
Happy to drive the console live for twenty minutes, or to pair on a real problem rather than a whiteboard exercise. lohan.johnson@outlook.com · 0773 391 6102
Appendix: stack
- HTTP API: FastAPI on uvicorn; pydantic v2 models, pydantic-settings config.
- Data: SQLAlchemy 2 over PostgreSQL (psycopg2), every query scoped by
tenant_idwith row-level security underneath. - Outbound and crypto: httpx for ERP calls; cryptography for sealing per-tenant credentials.
- Ops: typer + rich CLI; a resident worker draining an outbox under leases; prometheus-client metrics.
- Quality: pytest (~240 tests) and ruff; Locust + gevent in a separate virtualenv for load.
- In-account: SuiteScript 2.x inside the simulated NetSuite; React + Vite for the admin dashboard.
Every measurement on this page was taken on 22 October 2026, on an 8-core Apple laptop, with the platform and simulator running locally. Re-run with make loadtest-smoke / loadtest-sync and the verdict prints the same style of summary.