Select any text to highlight it or add a note. Notes stay on this device only. Use Notes → Export to back them up.
AI operating system for e-commerce
One stock ledger for every channel you sell on
Vervoro runs the daily operations of a marketplace seller with their own warehouse: catalogue, stock, orders, picking, dispatch and the stock numbers sent back to Amazon, eBay, Shopify and TikTok Shop.
It is a clean, multi-tenant rebuild of seba-erp, the in-house system Seba Trade runs today. Seba UK is tenant #1.
No application code yetP0 ≈ 36 engineer-daysSeba UK cutover: Jan 2027
PICKING SLIPORD-AMZ-20412 · FBM
SKU
EAN
Cases
Units
Lot · BB
OLV-500
…40213
1
12
L2207 · 03/27
OLV-500
…40213
0
4
L2231 · 05/27
TEA-GRN
…88105
—
6
20261002-01
15 × 10 cm slip, earliest best-before picked first (FEFO). Example data.
01-handover · 02-modules
A seller's day, in six steps
Everything below happens in one system, from a phone with the camera as a barcode scanner. Each step is guarded by domain rules that have their own tests.
1
Receive
Goods arrive against a supplier PO and become batches with lot, best-before and landed cost.
S11PU1
2
Import orders
Orders flow in from every connected account. FBA orders are recorded for reporting only.
O1O6
3
Reserve
One allocation service binds each order line to specific batches, earliest best-before first.
S2S6
4
Pick & pack
Pick lists and picking slips show cases and units. Printing them moves no stock.
S7S5
5
Dispatch
Confirming dispatch is the only thing that takes physical stock out, once.
S5O8
6
Push stock
Changed sellable quantities go back to each channel through the outbox.
seba-erp once pushed physical stock to Shopify and oversold. Vervoro only ever sends sellable stock, divided by the listing's pack size. Move the sliders to see what a channel would receive.
Listing fulfilment
Worked out
Physical saleable120
− expired10
− reserved18
− buffer5
Sellable (base units)87
÷ multiplier, rounded down6
Quantity pushed14
Only sent if it changed since the last confirmed push (E4). Never below zero, never clamped (S4).
05-domain-rules
Rules that cannot be broken
Every rule has at least one automated test named with its id. Several were learned the hard way in seba-erp; those bugs are listed so nobody repeats them.
S1
Ledger first
Every physical change is an immutable movement. Balances always equal the sum of movements, checked nightly.
S2
One reservation function
Every channel, manual order, Vendor PO and FBA shipment reserves through the same inventory service.
seba-erp: per-channel copies drifted and stranded reserved stock
S4
Never negative, never clamped
A movement that would go below zero fails loudly. It is never hidden by clamping to zero.
S7
Base units everywhere
Packs and boxes are multipliers that are always applied.
seba-erp: shipping five 10-packs deducted 5 instead of 50
O6
FBA orders never reserve
Amazon-fulfilled orders (about 85% of Seba's volume) are imported for reporting only.
E3
A timeout means unknown
Read the marketplace's state back before retrying. Never blindly resend a confirmation.
E6
One stock writer per account
During migration the old system writes and Vervoro watches in shadow mode. At cutover the roles swap.
C5
Never push to FBA listings
On Amazon a quantity on an FBA SKU can flip the listing to merchant-fulfilled.
V2
Vendor price ceiling
Confirmed cost ≤ PO cost × (1 + 2% tolerance), rounded down to the penny.
AI1
AI proposes, people approve
AI never changes data. It creates a proposal, which is re-checked against the current state when applied.
03-commercial-model
Three plans, one ceiling that never blocks
Plans differ by modules and by how many orders ship from the tenant's own warehouse. Users, channels, warehouses and the AI connector are unlimited on every plan.
Monthly, ex VAT · annual = 2 months free
Core
£99 / month
up to 1,500 own-warehouse orders / month
Products, inventory, orders, invoicing
Every channel connector
Basic reports and profit per SKU
AI connector (MCP)
Growth
£279 / month
up to 5,000 own-warehouse orders / month
Everything in Core
FBA shipments and purchasing
Detailed reporting, warehouse tasks
Staff attendance
Pro
£549 / month
up to 15,000 own-warehouse orders / month
Everything in Growth
Higher order ceiling
Priority support
Vendor Central add-on£199 / month on Growth or Pro.
No overage, everCeilings use a 3-month average. FBA orders never count, and imports and shipping never stop.
Founder rateFirst 25 customers: 25% off for 24 months, then 15% off for as long as they stay.
04-architecture
Serverless on Cloudflare, one shared database
A TypeScript modular monolith: three Workers from one repository, Neon Postgres in London, and row-level security enforced by the database itself.
Browser (React SPA) ──▶ web Worker ──▶ Hyperdrive ──▶ Neon Postgres (RLS)
Claude / ChatGPT ──MCP+OAuth──▶ mcp Worker ──▶ Hyperdrive
Marketplace + Stripe webhooks ──▶ web Worker ──▶ R2 (tenant files)
web Worker ──outbox──▶ Queues ──▶ jobs Worker
Dispatcher cron ──▶ jobs Worker ──▶ Workflows (long syncs)
jobs Worker ──▶ Durable Objects (rate limiters)
──▶ Amazon SP-API · eBay · Shopify · TikTok
web
The React app, the Hono API, sign-in, and inbound webhooks (verify, store, enqueue).
jobs
The single dispatcher cron, queue consumers, Workflows, rate limiters and the outbox relay.
mcp
The MCP server and its OAuth provider, so customers use their own Claude or ChatGPT.
Modules declare themselves in a manifest. A request succeeds only if the tenant is entitled, has the module enabled, and the user is permitted.
Tenant from the server
The tenant comes from the session, a signed job message or an MCP token. A tenant_id sent by the client is ignored.
Forced row-level security
Every tenant table has tenant_id and a forced RLS policy. A missing tenant matches no rows, never all rows.
One door for each path
Tenant work goes through withTenant. Cross-tenant work goes through withPlatform, which audits every use.
07-delivery · 13-migration
From seba-erp to Vervoro
Seba UK moves the same way every future customer will: import once, watch in shadow mode, then switch over in about an hour. No go-lives between 15 November and 31 December.
1
Discovery call
Channels, warehouses, SKUs, kits and packs, batches, Vendor and FBA use.
2
Fill Vervoro's templates
Vervoro has importers only; it never reads another system's format.
3
Import master data
Previewed, re-runnable, matched by SKU. No stock yet.
4
Connect channels in shadow
Orders arrive, listings auto-map. Nothing is sent to any channel.
5
Shadow check
Every order arrives correctly and the unmatched-SKU list is empty.
6
Cutover
Old sync off, stock imported once, connections switched to active one at a time.
7
Fallback window
The old system stays available for at least two weeks.
P0 · needed to move Seba UK
Auth with MFA, RBAC, RLS, inventory ledger, orders, purchasing, all four connectors, Vendor POs, the FBA wizard, phone-first screens and camera scanning.
P1 · weeks after cutover
MCP connector with approval screens, Vendor ASN labels, settlement import and reports, attendance.
P2 · before the first external customer
Stripe billing and order ceilings, self-serve sign-up, admin console, status page, the legal checklist.
Later
The Amazon Ads engine and accounting. Both stay in seba-erp until then.
09-costs
About $36 a month to run Seba UK
Infrastructure only, at October 2026 list prices. The database is almost the whole bill; Workers stay at the $5 base until the tenant count is large.
$0$100$200$300$400
At 50 tenants that is about $8 per tenant. The earlier VM design was budgeted at about €135 a month for one customer, plus server administration.
163 terms · source doc in brackets
Glossary
A
Accounting
Later module. Bank feeds, VAT returns, bookkeeping. Stays in seba-erp for now. [02]
Active mode
Connection mode after cutover: Vervoro is the stock writer. [13]
Ads engine
Amazon Ads automation, module ads. Lowest priority; stays in seba-erp. Acts only on single-ASIN ad groups after a person approves (A1). [02]
AFN / MFN
Amazon's fulfilment channel codes: AFN = Amazon-fulfilled, MFN = merchant-fulfilled. Matched positively; unknown values are not reserved. [05]
AI Gateway
Cloudflare proxy for in-app AI calls, metered per tenant. [04]
Allocation / reservation
Stock bound to an order line from a specific batch. An allocation row, not a movement and not just a counter (S2). [05]
Amazon Seller
Selling on Amazon as a third-party seller via Seller Central. Connector amazon-seller using SP-API. [06]
Amazon Vendor / Vendor Central
Selling wholesale to Amazon, which sends purchase orders. Vervoro imports, reviews and confirms them. [06]
Anonymiser
Job that removes buyer personal data per Amazon's data protection rules. P0, live from the first shadow connection. [07]
ASIN
Amazon's product id. Groups FBA and FBM SKUs of one listing. [05]
ASN
Advance Shipping Notice for Vendor shipments. Stays in Vendor Central at first; ASN and carton/pallet labels are P1. [07]
Attendance
Staff clock-in and leave. Growth module, built last. [02]
Audit log
Record of every mutation and every cross-tenant access. [04]
B
Bacs Direct Debit / SEPA
Bank debit payment methods for UK / EU. Bacs is capped at £4, so cheapest on annual plans. [09]
Base units
The smallest unit stock is counted in. Packs and boxes are multipliers that are always applied (S7). [05]
Batch
Stock received together, with lot, best-before, production date, supplier batch, units per box and landed cost. Balances by location and status. [05]
Best-before
Expiry date on a batch. Expired batches are never allocated or counted as sellable. [05]
Better Auth
Auth library with the organization plugin (organization = tenant) and twoFactor plugin, in its own auth schema. [04]
Box / pack configuration
Packaging levels such as 'Box × 12, Pallet × 480'. Pick documents show cases and units. [02]
Buy Box
The Amazon offer that wins the default 'Add to basket'. Tracked in detailed reporting. [02]
Buyer matching
Linking marketplace buyers to a customer only when name + email, or name + postcode, match. Postcode alone is never enough. [02]
C
Canonical order
One order per (tenant, connection, native_order_id). Re-import updates, never duplicates (O1). [05]
Carrier Central
Amazon's portal for booking Vendor freight. Transport stays there, as today. [01]
Channel
A place the seller sells: Amazon, eBay, Shopify, TikTok Shop, later Walmart, Target, Etsy. Unlimited on every plan. [02]
CI catalogue check
CI fails if any table outside platform and auth lacks tenant_id, row security or forced row security. [04]
Cloudflare Workers
Serverless runtime everything runs on. Three Workers: web, jobs, mcp. Workers Paid is $5/month. [04]
A foreign key that includes tenant_id, so a row can never point at another tenant's row. [04]
Compute unit (CU)
Neon's unit of database compute; billed per compute-hour. [09]
Connection / channel account
One connected account of a connector, with its own identity, token, status, default warehouse and sync progress. Reconnecting the same account keeps its history (C1). [05]
Entry plan: £99 / €119 / $129 a month, up to 1,500 own-warehouse orders a month. Products, inventory, orders, customers, invoicing, reports, channels, all connectors, AI connector. [03]
Custom distribution app
Shopify app type Vervoro uses; billing goes through Stripe, not Shopify. [09]
Cutover
The switch from the old system to Vervoro as stock writer, about an hour at a quiet time. [13]
D
Decision (D-0NN)
A dated owner decision in 08-decisions.md, e.g. D-053 MCP after cutover, D-065 January 2027 cutover. Only Live rows apply. [08]
Discovery call
First migration step: channels, warehouses, SKUs, kits, batches, Vendor and FBA use. [13]
Dispatch
Confirming the order has shipped. The only step that decreases physical stock; recorded once (S5, O8). [05]
dispatched_elsewhere
Status for an order the old system shipped during shadow mode. No stock effect. [13]
Dispatcher cron
The single Cron Trigger in the jobs Worker that reads every module manifest's schedules. Never add a cron per module. [04]
DPA
Data processing agreement between Seba Trade and Vervoro's company. Needed before the first shadow connection. [01]
Cloudflare single-instance stateful objects; used as API rate limiters. [04]
E
EAN / GTIN / UPC-A
Barcode numbers. Check digit validated; comparison normalises hyphens, whitespace and the dropped leading zero between EAN-13 and UPC-A. [02]
eBay item id / Shopify variant id
The channel's own product reference, used like ASIN to group SKUs and auto-map listings. [05]
Enablement
Gate 2 of 3: the tenant's admin has the module switched on. An admin can hide a paid module without cancelling it. [03]
Engineer-day
One engineer working one day; the unit of estimates. [07]
Entitlement
Gate 1 of 3: the tenant has an active right to a module, from Stripe, a trial or a manual grant. Stored as tenant_entitlement rows derived from Stripe events. [03]
Expand / contract migration
Schema changes that work with both old and new code. Migrations run over Neon's direct URL as the owner role, never via Hyperdrive. [04]
External customer / tenant
A paying customer that is not Seba. Comes after P2, the legal checklist and marketplace app approvals. [07]
F
Fallback
If cutover goes badly: connections back to shadow, old sync back on, re-enter stock changes by hand. Old system kept at least two weeks. [13]
FBA
Fulfilled by Amazon. Amazon stores and ships the stock. FBA orders are imported for reporting only (O6); Vervoro never pushes quantity to FBA listings (C5). [05]
FBA shipment / inbound plan
Moving own stock to Amazon's warehouses. A transfer, not a sale (S10): reserved when ready to pack, deducted at handover, keyed on Amazon's shipmentId. [05]
FBM
Fulfilled by Merchant. The seller ships from their own warehouse. The only listings that receive stock pushes. [05]
FEFO
First Expired, First Out. Allocation takes the earliest best-before first; manual override allowed and audited (S6). [05]
Forced RLS
FORCE ROW LEVEL SECURITY: the policy applies even to the table owner. [04]
Founder rate
First 25 paying customers: 25% off for 24 months (price fixed), then 15% off list for as long as they stay. Not on the Vendor add-on. [03]
Safe to repeat: running it twice has the same effect as once. Jobs use a stable operation id; API idempotency keys are stored per tenant and route with a body hash. [04]
Import templates
Vervoro's own file formats. Vervoro has importers only and never reads another system's format. [13]
import_run
Record of each import run, with preview and per-row errors. [13]
Internal tenant
Seba UK, Seba EU and demo tenants. Every module granted manually, no Stripe subscription. [03]
Isolation suite
CI tests that call every route, export, file, job and MCP tool as tenant B against tenant A's ids. Must return nothing or 404. [04]
J
Jobs view
Per-connection view of what each sync did, when, and what failed. [02]
jobs Worker
Cron, queue consumers, Workflows, rate-limiter Durable Objects and the outbox relay. [04]
K
Kit / bundle
Sold as one item, picked from component stock. Has no stock of its own. Kit sellable = min over components of ⌊component sellable ÷ quantity per kit⌋. [05]
L
Landed cost
Unit cost including all charges to get goods into the warehouse. Provisional until all charges arrive. [05]
Lane
One engineer's stream of work. Two engineers, two lanes. [07]
Ledger
The movements table. Balances must always equal the sum of movements, checked nightly (S1). [05]
Location / bin
A place inside a warehouse where a batch sits. [02]
Logpush / Workers Logs / Issues
Cloudflare logging and error tracking. Security logs kept in R2 for 12 months. [04]
Lot
The supplier's batch number. Without one, Vervoro generates YYYYMMDD-NN. [05]
How a module declares itself: routes, permissions, schedules, entitlement. Entitlement and enablement middleware sit on its mount point. [04]
Manual / B2B order
Direct, telephone or business order entered in Vervoro. Same reservation and dispatch rules as channel orders. [02]
Masked seed
Anonymised Seba-shaped data for previews and AI tools. Unmasked customer data never goes into an AI tool. [04]
MCP
Model Context Protocol. Lets a customer's own Claude or ChatGPT read Vervoro and create proposals. Costs Vervoro no tokens. P1, shortly after cutover. [04]
MCP token
Acts as one user in one tenant with that user's permissions; revocable; never exposes marketplace credentials (AI3). [05]
mcp Worker
The MCP server and its OAuth provider, served at /mcp* on the app host. [04]
MFA
Multi-factor sign-in. P0, required by Amazon data protection. [07]
Modular monolith
One repository and codebase, split into modules that declare themselves through manifests. [04]
Module
A sellable unit of features with a stable key (core, products, inventory, orders, warehouse, customers, invoicing, reports, channels, amazon-seller, ebay, shopify, tiktok, vendor, fba, purchasing, ads, reports-advanced, ai, attendance, accounting). Never rename a key. [02]
Movement
One immutable stock ledger entry: received, transferred, dispatched, adjusted, repacked_in/out, returned, written_off. Has reason, actor and source. [05]
Multiplier
How many base units one channel unit contains (a 6-pack listing = 6). Pushed quantity = ⌊sellable ÷ multiplier⌋. [05]
N
NDA / IP assignment
Signed by each engineer before receiving any of Seba's data, masked or not. [01]
Neon Postgres
Managed Postgres in London (aws-eu-west-2). Launch plan now, Scale plan before the first external tenant. Branches copy data for PR tests. [04]
O
Onboarding packages
Guided setup £595 (about 8 hours); full migration £1,950; extra work £75/hour. [03]
Order ceiling
The plan's limit on own-warehouse orders, measured as a 3-month average. Warn at 80%, upgrade at renewal. No overage charges, and importing and shipping never stop. [03]
Order status / stock status
Tracked separately. Flows are in the data model. [12]
Outbox
Table of things that must reach a marketplace, written in the same transaction as the change and sent later by an idempotent job (E2). [05]
Own-warehouse order
An order dispatched from the tenant's own warehouse, counted once in the month it is dispatched. Manual and B2B orders count; each Vendor PO shipment counts as one. FBA orders, cancellations, replacements and returns count zero. [03]
P
P0
Needed to move Seba UK: everything that writes stock or that Seba needs to fulfil orders. About 36 engineer-days. [07]
P1
In the weeks after cutover; doesn't write stock: MCP, Vendor ASN, settlement import, reports, attendance. [07]
P2
Before the first external customer: billing, sign-up, admin console, status page, legal checklist. [07]
Paste refresh token
Connect path for self-authorised apps (Seba's existing apps) until Vervoro's own apps are approved. [01]
Permission
Gate 3 of 3: the user's role grants the action. Enforced by requirePermission(...) on every route. [10]
Physical pack
A product with its own stock, made by a repack job or bought as packs. Dispatching it does not consume components again (S8). [05]
Physical stock
What is on the shelf. Leaves only at dispatch confirmation (S5). [05]
Pick list
List of what to pick across orders. Printing it moves no stock. [02]
Picking slip
A 15 × 10 cm slip per order modelled on the Vendor PO sheet: SKU, EAN, cases and units, FEFO lots. [02]
Plan
A bundle of modules plus an order ceiling. Plans are data: code checks module entitlements or limits, never a plan name. [03]
£549 / €649 / $699 a month, up to 15,000 own-warehouse orders. Growth with a higher ceiling and priority support. [03]
Product owner
The person who makes the fixed decisions, recorded with dates in Decisions (08). [08]
Progress checkpoint
Every 2–3 working days an agent writes progress/YYYY-MM-DD.md with package status, speed and a forecast. [07]
Proposal
How AI changes anything: exact content plus the target state it was based on. A person approves; applying re-checks the state (AI1, AI2). [05]
Purchasing
Suppliers, purchase orders, receiving and landed cost. In Growth, but P0 for Seba because Seba receives stock through POs. [02]
Q
Quarantine
Where returns go until a person inspects them as saleable, damaged or written off (S9). [05]
Queues
Cloudflare message queues between the outbox relay and job consumers. [04]
R
R2
Cloudflare object storage for files, labels, PDFs and backups. Keys start tenants/{tenant_id}/; nothing public. [04]
Rate limiter
Per account and operation, in a Durable Object. HTTP 429 means wait and retry (E5). [05]
RBAC
Role-based access control: roles grant permissions. Admin cannot grant Owner; nobody changes their own role. [10]
React + Vite SPA
The single-page front end, with TanStack Router and Query, Tailwind 4 and shadcn/ui. [04]
Reconcile
Periodically reading native quantities and orders back, because an HTTP 200 is not proof (E7). [05]
Reference module
modules/products, built first. Every other module copies its shape: manifest, routes, Zod schemas, requirePermission, audit entry, domain and isolation tests. [04]
Repack
Converting components into packs with paired REPACKED_OUT / REPACKED_IN movements. [05]
Reverse charge
EU B2B VAT rule; requires validating the customer's VAT number. [03]
RLS (row-level security)
Postgres feature that filters rows by policy. Every tenant table has RLS enabled and forced with tenant_id = nullif(current_setting('app.tenant_id', true), '')::uuid. [04]
Rule ids
Domain rule prefixes: T tenancy, S stock, O orders, E external systems, C channels, V Vendor, F FBA, PU purchasing, A ads, AI. Test names start with the id, e.g. 'S7 applies pack multiplier'. [05]
RuName
eBay's redirect URL name for OAuth. Vervoro needs its own. [01]
S
Safety buffer
Units held back from the sellable figure to avoid overselling. [05]
Seba EU
A separate company and separate tenant with one warehouse. Uses no system today; moves after Seba UK through the same migration steps. [01]
Seba Trade
The company whose in-house ERP (seba-erp) Vervoro rebuilds. Its companies are Vervoro's first tenants. [01]
Seba UK
Tenant #1. About 1,000 orders/day including FBA, about 150/day fulfilled from its own warehouse. Cutover expected January 2027. [01]
seba-erp
Seba's current single-company ERP. Source of business behaviour and edge cases, but not of architecture (singleton settings, globalThis caches, setInterval scheduler, plaintext secrets, unscoped queries). Read-only reference in reference/seba-erp/. [01]
Sellable stock
Physical saleable stock in the warehouses serving a channel account − reserved − safety buffer, non-expired batches only. What channels receive (S3). [05]
Seller Central's own FBA shipment flow. Shipments made there are still recorded in Vervoro; a daily check lists unrecorded ones. [05]
Settlement / Finances API
Amazon payout data. Feeds basic profit per SKU (Core) and payout profitability (Growth). P1. [07]
Shadow check
Done when every new order appears correctly and the unmatched-SKU list is empty. [13]
Shadow mode
Connection mode during migration: Vervoro reads orders and listings but sends nothing. Sends are stored as suppressed. A tenant with shadow connections sends no email or documents. [05]
Shortage
Not enough stock: reserve what is available and show what is missing. Never cancel automatically (O3). [05]
Stock Keeping Unit. Unique per tenant (T3). The match key for CSV and imports; never written back. [05]
SP-API
Amazon Selling Partner API, used for Seller and Vendor. Currently $0. [06]
Stock push
Sending the latest absolute sellable quantity to merchant-fulfilled listings whose quantity changed since the last confirmed push (E4). [05]
Stock writer
The one system and job allowed to push stock to a channel account at a time (E6). [05]
Stripe
Billing. Source of truth for what was bought. Checkout, Customer Portal, Stripe Tax, webhooks at /api/webhooks/stripe. [03]
suppressed
Outbox status for sends made in shadow mode. Never sent, even after the connection becomes active. [07]
T
Tenant
One customer company. The boundary for data and billing. A seller with a UK and an EU company is two tenants. [05]
Three gates
Entitled + enabled + permitted. The API enforces all three; the UI only hides what the API would refuse. [03]
Traceability page
Shows where a batch came from and where it went. [02]
Trial / grace / suspension
Tenant lifecycle states. Trial ends without a card: read-only 14 days. Payment fails: 14 days grace, then suspended up to 30 days, then cancelled. Channels are never left selling untracked stock. [03]
U
Unknown (timeout)
A timeout is not a failure. Read the native state back before retrying anything not naturally idempotent (E3). [05]
Unmanaged listings check
Before stock push is switched on, every active merchant listing with quantity is mapped, set to zero or signed off (C6). [05]
Unmatched SKUs
List of channel SKUs on incoming orders that have no mapping. Those lines reserve nothing until mapped (O2). [05]
V
Vendor Central add-on
Module key vendor. £199 / €229 / $249 a month on Growth or Pro. Vendor PO review and confirmation; ASN and labels later. [03]
Vendor PO
A purchase order from Amazon Vendor. Each confirmation needs explicit approval (V1); price ceiling rule V2. [05]
Vervoro
Multi-tenant SaaS operations system for marketplace sellers with their own warehouse: catalogue, stock, orders, warehouse work and channel integrations. A clean rebuild of seba-erp on Cloudflare. [01]
W
Warehouse
A stock site. Several per tenant from day one; new compared with seba-erp, which had only a flat list of locations. [02]
Warehouse scope
Limits which warehouses a user can act on. Has its own test suite. [10]
Warehouse tasks
Growth module (seba-erp's planner): title, assignee, priority, due date; TODO → IN_PROGRESS → DONE. Completing a task never moves stock. [02]
web Worker
Serves the React app, /api/*, auth routes and inbound webhooks (verify, store to inbox, enqueue). [04]
withPlatform
The one door for cross-tenant work, using the vervoro_platform role. Every use is audited. [04]
withTenant
The only way to touch tenant data: withTenant(tenantId, tx => …) sets app.tenant_id with set_config(…, true) inside the transaction, plus lock and statement timeouts. [04]
Work package
A P0 chunk with an engineer-day estimate (A1–A7 for lane A, B1–B8 for lane B). [07]
Vervoro is a multi-tenant operations platform for marketplace
sellers: catalogue, stock, orders, warehouse work and channel
integrations, sold as three plans (Core, Growth, Pro)
plus a Vendor Central add-on. It is a clean rebuild, on Cloudflare's
serverless platform, of the functionality proven in Seba Trade's
in-house ERP (seba-erp). Seba Trade is the first
tenant.
Status (2026-10-07): no application code yet. These
documents are the complete handover to the build team; they were
reviewed on 2026-10-07 and the findings fixed (reports in reviews/). Open owner questions are in
Decisions.
The standard way to move a seller from their old system: import
templates, shadow check, cutover
Reading time: about two and a half hours.
The engineer NDA and IP assignment draft is in legal/; it must
be signed before any of Seba's data is shared.
What is authoritative
Decisions: dated
owner decisions. They override everything below.
docs/ defines what to build
and the rules that cannot be broken.
seba-erp is the reference for how Seba works
today: behaviour, edge cases, marketplace quirks. When a document
says "seba-erp: lib/vendor-pricing.ts", read that file to
understand the behaviour. Without access to the seba-erp repository, the
same file is in reference/seba-erp/,
a curated copy of 51 files including the full data model. A read-only
login to the running seba-erp app shows the screens; reference/seba-erp/SCREENS.md
maps every page to its Vervoro module and priority. Do not copy
its architecture. It is single-tenant, keeps settings in one
row, has no tests and runs as one long-lived Node process.
Who decides
Engineers own every implementation choice that
stays within these documents, including library choices marked
"recommended". Record a change of a recommended choice in Decisions.
The product owner decides scope, pricing, anything
a customer would notice, and anything marked Fixed in the
architecture.
Product owner: Muhammet Surucu (owner of Seba
Trade). Engineers: Yusuf Figanioglu and Tolga
Sahin.
Maintaining these docs
Keep one fact in one place and link to it rather than copying. Update
the docs in the same pull request as a behaviour change. Keep them
short: if a document passes about four pages, split it or cut it.
Copy this file into the application repository on day one and keep it
current. It applies to code as well as to these docs.
Sources, in order of
authority
docs/08-decisions.md: dated owner decisions.
Only rows whose Status is Live (or Proposed and confirmed)
apply; ignore superseded rows.
docs/ (01–13): what to build and the rules that cannot
be broken. The data model is docs/12-data-model.md;
permissions are docs/10-permissions.md; moving a customer's
data in is docs/13-customer-migration.md.
seba-erp (read-only login to the app, and the copy in
reference/seba-erp/): how Seba's business works today. Read
it for behaviour and edge cases. Do not copy its architecture or
patterns: singleton settings, globalThis caches,
setInterval scheduler, plaintext secrets, unscoped queries,
a hard-coded channel enum.
If two sources conflict, stop and ask. Do not quietly pick one.
Stack (see
docs/04-architecture.md)
TypeScript only. Cloudflare Workers (web,
jobs, mcp), Hono + Zod, React + Vite SPA,
Drizzle with pg, Neon Postgres (London) through Hyperdrive
with caching off, Better Auth (organization = tenant, own
auth schema, twoFactor plugin), Queues,
Workflows, Durable Objects, Workers KV (MCP OAuth grants), R2, Workers
Logs + Issues, Logpush, Stripe. Don't add a server, VM, container or new
infrastructure service without a decision entry.
Worker config:compatibility_flags: ["nodejs_compat"] and a pinned
compatibility_date in every
wrangler.jsonc.
Clients per request: create the pg
client and the Better Auth instance per request or per job from the
bindings, never at module scope; close the client with
ctx.waitUntil(client.end()).
Schedules: one dispatcher cron in the
jobs Worker reads every module manifest's
schedules. Never add a Cron Trigger per module.
MCP: the mcp Worker serves
/mcp* on the app host so consent can see the Better Auth
session.
Commands
Fill these in on day 1 and keep them true; an agent without commands
guesses.
Task
Command
Install
to fill
Run locally
to fill
Unit and integration tests
to fill
Tenant-isolation and warehouse-scope suites
to fill
Generate a migration
to fill (drizzle-kit generate, then review the
SQL)
Apply migrations
to fill (Neon direct URL, owner role)
Deploy staging / production
to fill
Migrations
Change the Drizzle schema, run drizzle-kit generate,
and read the SQL.
Add by hand, in the same migration:
ALTER TABLE … FORCE ROW LEVEL SECURITY, the policy
tenant_id = nullif(current_setting('app.tenant_id', true), '')::uuid
for USING and WITH CHECK, and grants to the
runtime role.
Only expand/contract changes: a migration must work with both the
old and the new code.
Apply over Neon's direct URL as the owner role,
never through Hyperdrive.
The CI catalogue check must pass: every table outside
platform and auth has tenant_id,
row security and forced row security.
Never
Trust a tenant id from the client. The tenant comes from the
session, a signed job message or an MCP token.
Touch the database outside
withTenant(tenantId, tx => …), or set the tenant with a
session-level SET (Hyperdrive resets it; use
set_config('app.tenant_id', …, true) inside the
transaction).
Do cross-tenant work any way except
withPlatform(reason, …) and its named role. Never use the
owner role or BYPASSRLS at runtime.
Create a tenant table without tenant_id, forced RLS,
composite tenant foreign keys and tenant-scoped unique constraints.
Turn on Hyperdrive query caching, or write a tenant query without an
explicit tenant_id = $1 filter.
Run migrations through Hyperdrive, or create a preview database
branch from anything except the masked seed.
Let staging, or any connection in shadow mode, send
anything to a real marketplace or customer. Shadow sends are stored as
suppressed and never sent, not even when the connection
later becomes active. A tenant with shadow connections
sends no email or documents.
Put a customer's unmasked import files, production query
results, tokens or buyer personal data into an AI tool's
context. Use the masked seed or synthetic data.
Write stock tables from anywhere except the inventory service.
Make a network call inside a database transaction.
Send anything to a marketplace except through the outbox and an
idempotent job.
Blindly retry a non-idempotent external operation after a timeout.
Read the native state first.
Push physical stock to a channel, or push any quantity to an FBA
(Amazon-fulfilled) listing. Push sellable stock, divided by the
mapping's multiplier, to merchant-fulfilled listings only.
Clamp a negative stock result to zero. Fail instead.
Add money across currencies, or use floating point for money.
Let AI or the ads engine change data without an approved proposal;
return buyer personal data or unfiltered cost fields from an MCP
tool.
Store marketplace tokens unencrypted, or log tokens or buyer
addresses.
Serve an uploaded file inline from the app's origin.
Check a plan name in code (if plan == "growth"). Check
a module entitlement or a limit quantity instead; plans are data.
Grant admin rights by default ("everyone is admin while no admin
exists" is forbidden), let an Admin grant Owner, or let anyone change
their own role.
Always
Copy the shape of the reference module
(modules/products, built first): manifest, routes, Zod
schemas, requirePermission, audit entry, domain tests and
isolation tests.
Put a module behind its manifest: entitlement and enablement
middleware on its mount point, and requirePermission(…) on
every route.
Write a test for each domain rule you implement
(docs/05-domain-rules.md, using its rule ids in test names,
e.g. S7 applies pack multiplier).
Add every new route, job, export, file route and MCP tool to the
tenant-isolation suite, and every warehouse-bound route to the
warehouse-scope suite.
Declare a new module's permissions in its manifest and add them to
docs/10-permissions.md; until roles are decided, only Owner
and Admin get them.
Store idempotency keys per tenant and route with a body hash
(docs/04-architecture.md API conventions).
Record mutations in the audit log.
Keep the docs current in the same pull request when behaviour or a
decision changes.
Before writing a connector, read seba-erp's version and the
provider's current docs, and record anything learned in
docs/06-integrations.md.
Vervoro is a SaaS operations system for sellers on Amazon (Seller and
Vendor), eBay, Shopify and TikTok Shop, starting in the UK and EU. It
covers what a seller with their own warehouse does every day: keep the
catalogue, receive stock, take orders from every channel, reserve and
pick stock, dispatch, push sellable quantities back to the channels,
ship to Amazon FBA, confirm Amazon Vendor POs, and later run Amazon
advertising.
Each customer company is a tenant. A tenant
subscribes to one of three plans (Core,
Growth, Pro) that differ by features
and by how many orders it ships from its own warehouse; Amazon Vendor
Central is an add-on. Channels, users and the AI connector are unlimited
in every plan. See Product and
modules.
Seba's workload
Company
All orders (incl. FBA)
Orders Vervoro fulfils (non-FBA)
Seba UK
~1,000/day (~30k/month)
~150/day
Seba EU
About the same as Seba UK
About the same as Seba UK
FBA orders (about 85% of the total) are imported for reporting and
dashboards only. They never reserve or move own-warehouse stock (rule
O6).
The fulfilment path that touches stock handles about 150 orders a
day per company. That is small for Postgres; size for bursts (e.g. Prime
Day) and imports, not for steady load.
Seba EU is a separate company and a separate tenant with one
warehouse. It uses no system today and moves after Seba UK, through the
same customer migration steps
(D-026, D-027, D-034, D-035).
Why a rebuild
seba-erp works and holds months of hard-won marketplace knowledge. It
was built for one company:
Single-company design: settings are a single row
(id: "singleton"), product SKUs are unique across the whole
database, and none of its 306+ queries filter by tenant.
Long-lived process: it needs one Node process (an
in-memory scheduler and caches) and stores marketplace secrets in plain
text.
No tests.
Vervoro keeps the knowledge and replaces the foundations:
multi-tenancy, RBAC, sellable modules and a serverless runtime from day
one.
The first goal: Seba
UK on Vervoro
Seba UK runs its daily operations on Vervoro, as
tenant #1, on production infrastructure, with seba-erp switched off as a
stock writer. The build is P0 in the delivery plan, estimated at about 36
engineer-days and tracked at regular progress
checkpoints. Seba UK then moves with the standard customer migration (D-058): import
once, a few days of shadow mode, then switch over. Expected cutover:
January 2027 (D-065).
Seba must be able to:
Do the warehouse and order work from a phone, scanning barcodes with
the camera.
Find any product, its batches, best-before dates and locations.
Receive goods against a supplier PO into batches.
See Amazon (FBM and FBA), eBay, Shopify and TikTok orders arrive,
with stock reserved automatically.
Create manual and B2B orders and their invoices.
Print pick lists and picking slips, confirm dispatch, and see stock
decrease.
See correct sellable stock pushed to every connected channel.
Approve and confirm Amazon Vendor POs at the right price, reserve
and ship them (ASN and transport stay in Vendor Central and Carrier
Central, as today).
Build and ship an FBA replenishment step by step, with stock
reserved and moved correctly.
Raise supplier purchase orders and receive against them.
Use Claude or ChatGPT through the MCP connector to look at products
and stock and to propose listing changes and purchase orders, which a
person approves (shortly after cutover, D-053).
Manage users and what each user may do.
What stays in seba-erp after cutover:
Accounting and the Amazon Ads
engine (both later modules).
All history before cutover (read-only).
Any P1/P2 module not finished in time (see the delivery plan).
Definition of done
(cutover)
Not in the first cutover
Accounting and the Amazon Ads engine (later modules; both stay in
seba-erp).
AI features that run inside Vervoro (P1/P2). The MCP connector
follows shortly after cutover (D-053).
Self-serve sign-up and Stripe checkout. The data model for
entitlements is built early in the build; the checkout UI follows Seba's
cutover.
External customers. These also depend on marketplace app approvals;
see Integrations.
The larger features from the earlier V1 design (central listing and
price management, promotions, DE/GB stock pools, condition-graded book
stock, formal stock counts). See Decisions.
Day-1 access checklist
The product owner provides these before day 1. Share secrets through
a password manager, never in chat or email.
Item
For
Status
Signed NDA and IP assignment for each engineer (draft)
Legal. Before engineers receive any of Seba's data, masked
or not
open
Data processing agreement between Seba Trade and the company running
Vervoro (arranged by the product owner, D-054)
Legal basis for Vervoro holding Seba's data. Before the
first shadow connection (about build day 4–5)
open
Read-only login to the running seba-erp web app
Seeing current screens and behaviour
Confirmed
This docs repository, including
reference/seba-erp/
The handover
Ready
GitHub organisation and the new application repository
Code, CI, nightly backup job
open
Cloudflare account on Workers Paid, with the Vervoro domain
added
Hosting
open
Neon organisation with a London project
Database
open
Stripe account in test mode
Billing (P2)
open
Seba's existing app credentials, used as a bridge
until Vervoro's own apps are approved: the Amazon SP-API private app
(Seller), the separate Amazon Vendor app, and Seba's eBay app keys
Seba's connections from the first shadow day.
Self-authorised apps need a "paste refresh token"
connect path in Vervoro. Vervoro gets its own
authorisation on each channel (Integrations):
a second Amazon self-authorisation of each app, its own eBay RuName, its
own Shopify custom-distribution app, and a second TikTok self-developed
app. Check the Amazon apps' next client-secret rotation date
open
Vervoro's own developer accounts and apps (Amazon, eBay, Shopify,
TikTok)
External customers (Integrations).
The Shopify Partner account is needed by about day 6,
for Seba's shadow connection
open
A company-managed, encrypted machine for handling Seba's unmasked
files
Data protection
open
A named person at Seba for warehouse questions and the shadow
check
Migration and cutover
open
Working agreement
Two engineers, AI-assisted. Copy AGENTS.md into the application
repository on day one. AI agents follow it literally, so keep it
current.
Tests come with the code. Every domain rule in Domain rules gets at least one test.
Tenant isolation has its own suite that runs in CI.
When something is unclear: check seba-erp's
behaviour first. If it is still unclear and a customer would notice the
answer, ask the product owner and record the answer in Decisions.
Demo to the product owner on Seba's real data every
two to three days.
Biggest risks
Risk
Mitigation
The build takes longer than estimated
Progress checkpoints show it early; P1 items don't touch stock and
can stay in seba-erp past cutover
A tenancy bug leaks one seller's data to another
Database RLS plus server-only tenant context plus the isolation
suite. A bug here is a data breach, not a bug
Both systems write stock during the transition
Vervoro's connections stay in shadow mode until cutover; one writer
per channel account (rule E6)
Marketplace approvals block external customers
Start them now; Seba connects through its own existing apps
meanwhile
Workers limits (memory, CPU, connections) hit by heavy jobs
Three plans that differ by features, each with a
ceiling on orders shipped from the tenant's own
warehouse, plus one add-on. Full terms and prices: Commercial model.
Plan
What it adds
Core
Everything a multichannel seller needs to run stock and orders
Unlimited in every plan: users, channels and channel
accounts, warehouses, batch and best-before tracking, and the AI
connector (MCP). New channels added later (Walmart, Target, Etsy…) are
included in every plan too.
Module catalogue
key is the stable identifier used in code, permissions,
Stripe metadata and URLs. Never rename one.
key
Module
Plan
Depends on
seba-erp reference
core
Dashboard, scan, settings, users and roles, jobs view
Plans are bundles of these modules. Entitlements
stay per module in code, so a plan can be re-cut without code changes.
Seba UK and Seba EU are internal tenants with every module granted.
Purchasing for Seba: Seba receives stock through
POs, so Purchasing is P0 for cutover even though it sits in Growth.
Every tenant can always receive stock through Inventory ("goods in"
without a PO, with a required reason, audited and listed in the
reconciliation report); Purchasing adds the supplier and PO workflow on
top.
Core plan (all plans)
Warehouse tasks are listed here with customers for reading
convenience but belong to Growth.
Bundle / kit: sold as one item, picked from
component stock; it has no stock of its own.
Physical pack: has its own stock, made by a repack
job (REPACKED_IN/REPACKED_OUT) or received
from the supplier.
Box/pack configurations ("Box × 12, Pallet × 480").
Picking documents express quantities in cases as well as units.
Categories, tags, and a per-product activity
log.
Bulk edit by CSV. SKU is the match key and is never
written back. A blank cell means "leave alone", not "clear". (seba-erp:
lib/product-csv.ts)
Catalogue onboarding: pull drafts from a connected
channel into a workbook to correct, then import. SKUs already matched
are skipped. (lib/product-draft.ts,
lib/product-workbook.ts)
Barcode comparison normalises formatting: hyphens,
a dropped leading zero (EAN-13 vs UPC-A), whitespace.
(lib/barcode.ts)
Inventory
(inventory)
Batches carry: lot number, best-before, production
date, supplier batch, units per box and landed unit cost (provisional
until all charges arrive). A batch's stock sits in balance rows by
location and status (Data model). Batches
received without a lot get YYYYMMDD-NN references.
Warehouses and locations / bins. A tenant can have
several warehouses from the first version (Seba EU has its own).
Locations belong to a warehouse; each channel account has a default
warehouse (rule O9). seba-erp has no warehouse entity:
only a flat list of locations (WarehouseLocation), with no
warehouse on orders, so this is new in Vervoro.
Movements ledger: received, transferred,
dispatched, adjusted, repacked in/out, returned, written off. Every
movement has a reason, an actor and a source. Reservations are
allocations, not movements (rule S2).
Transfers between locations. Adjustments need a
reason.
FEFO: allocation picks the earliest best-before
first, with manual override.
Batch traceability page: where a batch came from
and where it went.
Returns: received into quarantine, then inspected
as saleable, damaged or written off.
Low-stock alerts per product.
Orders (orders)
One order list across channels, filterable by
channel account, status and stock status.
Imports from every connected channel account. FBA
orders are recorded for reporting but never reserve own stock.
Manual orders: direct, telephone and B2B. They use
the same reservation and dispatch rules as channel orders.
Reservation on import:
Partial reservation when stock is short; the missing quantity is
visible.
Unmapped SKUs are flagged and reserve nothing.
Picking and dispatch documents: pick list, packing
list, and a 15 × 10 cm picking slip modelled on the Vendor PO sheet
(SKU, EAN, cases and units, FEFO lots).
Dispatch:
Confirming dispatch is what decreases physical stock.
Tracking is recorded on dispatch.
The channel is notified of dispatch where the connector supports
it.
Basic profit per SKU from Amazon payouts. The
detailed version (full payout profitability with landed cost, Buy Box,
rank tracking) is in Growth. Both need the Amazon settlement import
(P1). Ad spend joins the profit figures only once the Ads module
exists.
Channels (channels)
Connections:
Several accounts per connector, each with its own identity, token,
status and sync progress.
Reconnecting the same account keeps its history.
SKU mapping:
Channel SKU → product, per connection.
An "unmatched SKUs" list fills from incoming orders.
Listings are grouped by the channel's own reference (ASIN, eBay item
id, Shopify variant id), so FBA and FBM SKUs of one ASIN fold
together.
Stock push: sellable quantity only, only the SKUs
that changed. See Domain
rules.
Jobs view: what each sync did, when, and what
failed, per connection.
Growth, Pro and add-on
modules
FBA, purchasing, detailed reporting and attendance come with Growth
and Pro. Vendor Central is an add-on on Growth or Pro. AI & MCP is
in every plan.
Amazon Vendor
Central (vendor): add-on
POs: synced from Vendor Central, with changes and
cancellations.
Review and confirmation:
A user reviews each PO line: accept a quantity, reject the
rest.
The confirmed price must be at or below the price ceiling defined in
rule V2.
A "check prices" step compares against Vendor Central.
Approval creates a real order on the Vendor channel
and reserves stock. From there it uses the normal dispatch flow.
(lib/vendor-stock.ts)
Shipping: barcode matching (as in seba-erp). Carton
and pallet labels and the ASN come later (P1); seba-erp doesn't have
them, and Seba does them in Vendor Central and Carrier Central
today.
Multiple Vendor accounts per tenant (e.g. Vendor
UK, Vendor Germany).
FBA shipments
(fba): Growth
Inbound plans built step by step on Amazon's
Inbound API (v2024-03-20), showing placement and transport options with
destination and carrier names. Prep and labelling come from Amazon's
per-SKU rule; see lib/fba-test.ts for why the one-click
builder failed.
Box labels: a "heavy package / team lift" warning
is added to boxes over 15 kg. Labels are fetched one per page in thermal
format, then laid out on A4. (lib/fba-labels.ts)
Stock: shipping to FBA is a transfer, not a
sale (rule S10). Stock is reserved once the shipment is ready
to pack and deducted at handover, with the pack multiplier applied; no
order is created. (lib/fba-stock.ts)
Status: shipment status is synced; ship-from
addresses come from Seller Central.
Purchasing
(purchasing): Growth
Suppliers: each supplier's own product codes are
learned once and remembered. An unknown code stops the order until a
person maps it; nothing is guessed.
Purchase orders: statuses as in Data model: Status flows.
Orders can be emailed to the supplier, and documents can be
attached.
Receiving creates batches with landed cost.
Landed cost allocation:
A discount or charge printed on the supplier document is shared by
value.
Freight, duty and clearance arriving on later invoices are shared
per unit by default, with value or weight as options.
(lib/purchasing/allocate.ts)
Supplier document → PO:
Lines are read from an invoice, proforma or delivery note.
The reading is accepted only if every line reproduces its printed
amount and the lines reach the document total exactly; otherwise nothing
is offered. (lib/purchasing/document-lines.ts)
Reorder suggestions combine stock cover in days,
recent sales, stock on order and supplier lead time.
(lib/purchasing.ts)
Warnings for likely mapping mistakes before an
order is approved. (lib/purchasing/checks.ts)
Amazon Ads
engine (ads): later, lowest priority
Not in the first version. Seba keeps using
seba-erp's Ads engine (its ads-* jobs stay switched on
after cutover) until this module is built. Ads never touches stock, so
running it in seba-erp alongside Vervoro is safe. The description below
records what the module must eventually do.
Sync and spend:
Ads profiles per marketplace.
A local mirror of the Sponsored Products structure (campaigns, ad
groups, keywords, targets).
Daily spend per SKU from asynchronous reports. A first report can
take 20 minutes.
Goals and strategies:
Bid engine: a target TACoS per ASIN is turned into
keyword bids and budgets through the ASIN's ad share of sales.
(lib/ads-engine.ts)
Strategies: named settings of six levers, scored
against the product's real numbers. (lib/ads-strategies.ts,
lib/ads-strategy-build.ts)
New campaigns: an auto campaign plus an exact
campaign. Opening bid = price × target ACoS × assumed conversion rate,
shown and editable.
Ongoing optimisation:
Harvesting: search terms proven over 30 days are
promoted to exact and negated where they were found.
Pacing: detects campaigns that run out of budget
and at what time of day.
Scope: the engine only acts on ad groups that
advertise this one ASIN; shared ad groups are reported, not touched.
(lib/ads-scope.ts)
Change control and history:
Every bid, budget and harvest change is a proposal a person
approves.
seba-erp prefixes the campaigns it creates ERP-.
Vervoro prefixes the campaigns it creates VRV-
(configurable per tenant) and treats Seba's existing ERP-
campaigns as legacy (D-048).
Campaign history merges Amazon's change history (90-day limit) with
Vervoro's own proposals and approvals.
Detailed
reporting (reports-advanced): Growth
Payout profitability: per Amazon settlement,
product by product. Amazon's fees and payments, plus cost of goods from
batch landed cost, minus VAT; plus ad spend per SKU once the Ads module
exists. (lib/settlement-profit.ts)
Pricing and Buy Box research per product.
Sales rank check and rank scan.
AI & MCP (ai):
every plan
Priority: the MCP connector, with its read tools,
propose tools and the approval screen, comes shortly after Seba UK's
cutover (P1, D-053). AI features that run inside Vervoro itself, and so
spend Vervoro's tokens, come later (P1/P2).
MCP connector lets Claude, ChatGPT and other
assistants work with the tenant's data:
Users sign in through OAuth, using public clients with PKCE
only.
Tokens are revocable, act for that user, and never carry a
marketplace credential. (lib/oauth.ts)
MCP tools:
Read tools: products, Amazon listing, variation
family, stock and sales, open POs, A+ content.
First release (P1, after cutover): the read tools,
plus the listing-change and purchase-order proposals with their approval
screens. The other propose tools follow.
With the Ads module (later): ads for a product,
search terms, ads change proposals.
Proposals never apply themselves:
A person reviews and approves each one.
Applying re-checks that the target has not changed since the
proposal was written. (lib/amazon/listing-edit.ts)
Images attached to proposals are copied into
storage. They are fetched only from public https addresses, never
internal ones. (lib/ai-images.ts)
Usage: AI usage is metered per tenant.
Attendance
(attendance): Growth
Clocking: staff clock in and out by scanning an
entrance code with their own phone; records are keyed to the signed-in
person.
Leave: annual leave requests, allowances and bank
holidays.
Dates are calendar days in the tenant's timezone,
so clock changes can't move them.
(lib/attendance-calc.ts)
Access: staff see only their own records; managers
see the team, correct records and approve leave.
Later: Accounting
seba-erp's accounting is large (about 12k lines) and specific to UK
bookkeeping (Xero/Dext, HMRC VAT, Amazon settlements, bank coding,
Vendor remittances). It stays in seba-erp for Seba UK and will become
Vervoro modules later.
Accounting differs by region, so plan it as one shared core
plus regional packs:
Part
Contents
Examples
Accounting core (shared)
Marketplace money into accounting-ready figures: settlements and
fees per channel, sales by country and currency, cost of goods from
batch landed cost, monthly profit
Amazon settlement reading, cost-of-goods report
UK pack
UK VAT and bookkeeping
Making Tax Digital VAT, import VAT (C79/postponed), Xero and Dext
export
EU pack(s)
Per-country VAT and local bookkeeping
EU VAT, One-Stop-Shop, Germany's DATEV export
US pack
Sales tax and US books
Sales tax by state (likely through a tax service), QuickBooks
export
Integrate with the customer's accounting software rather
than replacing it. Vervoro prepares and exports the figures and
documents, and Xero, DATEV or QuickBooks keeps the statutory books.
Building full statutory accounting for every country is a different
product.
Each regional pack is its own sellable module, offered to tenants in
that region.
Don't design anything now that blocks this. From the
first version:
keep landed cost on batches;
keep each tenant's Amazon settlement data;
store money with its currency;
on order lines, store tax amount, rate and country exactly as the
channel reports them;
give each tenant a country, a base currency and a list of its tax
registrations.
Prices and terms below were chosen by the product owner on 2026-10-07
from the pricing
debate. Keep every number in configuration, not code, so it can
change without a release.
Plans and add-ons
Core
Growth
Pro
Own-warehouse orders / month (3-month average)
up to 1,500
up to 5,000
up to 15,000
GBP / month (ex VAT)
£99
£279
£549
EUR / month
€119
€329
€649
USD / month
$129
$359
$699
Annual (2 months free)
£990 · €1,190 · $1,290
£2,790 · €3,290 · $3,590
£5,490 · €6,490 · $6,990
Modules
core, products, inventory, orders, customers, invoicing, reports,
channels, all connectors, ai
Add-on: Amazon Vendor Central (vendor)
on Growth or Pro: £199 · €229 · $249 per month.
Above 15,000 own-warehouse orders: priced on
request.
Unlimited in every plan: users, channels and
channel accounts, warehouses, batch and best-before tracking, AI
connector.
Optional onboarding: guided setup £595 (about 8
hours); full migration £1,950 (one source system, up to 2,000 SKUs and 4
channel accounts); extra work £75/hour, quoted first. Credited against
the first year if the customer pays annually.
How the order ceiling
works
Count orders shipped from the tenant's own
warehouses per calendar month: dispatched orders, each counted
once however many parcels it ships in, in the month it is dispatched
(there is no partial dispatch in V1). Manual and B2B orders count like
channel orders. Each Vendor PO shipment counts as one order. FBA
orders never count. Orders cancelled before dispatch,
replacements and returns count zero.
Use the average of the last 3 months, so one busy
December does not change the plan.
Warn at 80% of the ceiling. When the 3-month average is over the
ceiling, offer the next plan and move the tenant up at the next renewal.
No overage charges, ever.
Never stop importing or shipping orders because of
a limit.
Founder rate
(first 25 paying customers)
25% off the plan for 24 months, then 15% off for as long as the
customer stays subscribed. Written into the contract, with a price lock
and the right to export all data at any time.
Not applied to the Vendor add-on; not combined with the annual
2-months-free.
The first 10 founders who pay annually get the migration package
credited.
Founders agree to a case study or two reference calls a year.
Promises to every
customer
Prices rise at most once a year, by no more than 10%, with 90 days'
notice.
If Amazon reintroduces developer API fees, they may be passed on at
cost in proportion to each customer's usage, with 60 days' notice.
Commercial defaults
These fill gaps the pricing decision left open; the owner confirmed
them (D-055).
Founder lock vs the 10% cap: a founder's price is
fixed for the first 24 months (no rises at all). After
that it is always the current list price minus 15%, and list prices
follow the 10%-a-year cap. In Stripe: founder Prices fixed for 24
months, then a 15% coupon on list Prices.
Trial ends without a card: the tenant becomes
read-only for 14 days with a banner. Order import, reservation and
dispatch keep running, so channels never sell stock nobody tracks. Then
stock push is switched off after the user confirms "set channel stock to
0", and the tenant is cancelled.
Payment fails: 14 days grace with full access, then
suspended for up to 30 days (see Tenant lifecycle), then cancelled.
Before cancellation the owner is asked to set channel stock to 0;
Vervoro never leaves a channel selling stock it no longer tracks.
Vendor add-on ends: confirmed Vendor POs that are
already reserved can still be dispatched and closed; no new POs are
confirmed.
A seller with a UK and an EU company: two tenants,
two subscriptions; each company is billed separately.
Other rules
Plan changes: upgrades apply immediately, prorated;
downgrades apply at the end of the billing period.
AI: the MCP connector runs on the customer's own
Claude or ChatGPT, so it costs Vervoro no tokens and is in every plan.
AI features that run inside Vervoro (later) may come with a usage
allowance, metered per tenant through AI Gateway.
Internal tenants (Seba UK, Seba EU, demo tenants)
get every module granted manually, with no Stripe subscription.
Payments: prices are shown ex VAT. Offer Bacs
Direct Debit (UK) and SEPA (EU) alongside cards, especially for annual
plans. Register for UK VAT from day one, validate EU VAT numbers for
reverse charge, and keep a EUR balance in Stripe. Offer USD only to
customers with a UK or EU company until US channels launch.
Service credits for outages are capped at one
month's fee and are the only remedy; consequential losses are excluded.
A lawyer must check the wording.
Stripe
Products and prices: one Stripe Product per plan
(Core, Growth, Pro) and one for the Vendor add-on, each with GBP, EUR
and USD prices, monthly and annual. Onboarding packages are
one-off Prices. Each Price carries
metadata.vervoro_key (core,
growth, pro, vendor) and
metadata.kind (plan or addon).
Vervoro maps a plan to its module entitlements in configuration.
Founder rate: founder Prices fixed for 24 months,
then a 15% coupon on list Prices (see Commercial defaults), so the lock
survives plan upgrades.
Purchase and management: Checkout for the first
purchase, plan upgrades and adding Vendor. The Customer Portal handles
plan changes, payment methods and invoices.
Tax: Stripe Tax handles UK VAT and EU VAT. Collect
a VAT ID for B2B reverse charge.
Webhooks go to /api/webhooks/stripe:
Verify the signature.
Store the event by id before processing; processing is
idempotent.
Handle at least: checkout.session.completed,
customer.subscription.created|updated|deleted,
invoice.paid, invoice.payment_failed.
Stripe is the source of truth for what was bought.
Vervoro derives tenant_entitlement rows from the
subscription after every relevant event. Never read Stripe live on a
user request.
Three gates on every
request
A request to a module succeeds only if all three are true:
Entitled: the tenant has an active entitlement for
the module, from Stripe, a trial or a manual grant.
Enabled: the tenant's admin has the module switched
on. An admin can hide a paid module from the team without cancelling
it.
Permitted: the user's role grants the permission
the action needs. See Architecture:
RBAC.
The API enforces all three. The UI only hides what the API would
refuse.
Everything is built as an add-on; a plan is only a named bundle. That
way, who gets what can change without code: move a module between plans,
sell one on its own, or add a new plan.
The unit of sale is the module (key
from the module
catalogue). Code only ever checks "is module X entitled for this
tenant?", never "is this tenant on Growth?". Checking a plan
name in code is forbidden.
Limits are entitlements too, with a quantity:
limit.own_warehouse_orders = 5,000, and later, if wanted,
limit.channel_accounts or limit.warehouses.
Code reads the quantity, never a plan.
The plan catalogue is data, versioned, in the
platform schema:
plan: key, version, name, order
ceiling, the modules and limits it grants, its Stripe prices.
addon: key, the module or limit it
grants, which plans it can be added to, its Stripe prices.
Entitlements are derived from the tenant's
subscription: (plan version → modules + limits) + add-ons + manual
grants (internal tenants, goodwill) + trial. Recompute after every
Stripe event or catalogue change.
Changing who gets what: publish a new catalogue
version. Existing subscribers keep their current version (grandfathered)
until they change plan, or until a migration you choose with the
price-rise notice in "Promises to every customer".
Selling a module separately (e.g. detailed
reporting as its own add-on): add an addon row and its
Stripe price. No code change.
Design rule: anything that might one day be sold on
its own must be its own module key, with its own routes, jobs,
permissions and navigation behind the manifest. If unsure, split
it.
When an entitlement ends
Downgrades must never corrupt stock or leave a channel showing
quantities nobody maintains.
Day
What happens
0
Module becomes read-only. Its jobs stop. For a
channel connection (when the whole tenant is suspended
or cancelled), stock push is switched off on purpose: the user is warned
that the channel keeps its last quantity, and is offered "set channel
stock to 0"
0–30
Data visible and exportable; re-subscribing restores everything
30+
Module hidden. Data is kept until the tenant is deleted
Sign-up and onboarding
(self-serve)
Account and company: sign up with email or Google.
Turnstile protects the form. Create the company (tenant), pick a country
and currency, and the user becomes Owner.
Trial: 14 days on Growth, extended to 30 days once
the tenant has imported its catalogue. No card is needed during the
founding phase (revisit when self-serve and the Shopify App Store open).
One trial per company, matched on Amazon seller ID and VAT number. While
the unlisted Amazon app is limited to 25 sellers, Amazon is connected
with someone from Vervoro; a demo tenant with sample Amazon, FBA and
Vendor data lets prospects explore first.
Onboarding wizard:
Warehouse and locations.
Connect the first channel.
Import the catalogue from that channel (workbook flow).
Opening stock.
Invite users.
Each step can be skipped and resumed.
Go live: connections start in shadow
(reading, sending nothing). Switching one to active turns
on stock push and is a separate, explicit step after the
unmanaged-listings check (rule C6), because it overwrites channel
quantities. A seller moving from another system follows Customer migration.
Tenant lifecycle
State
Meaning
Access
trialing
In trial
Full
active
Paid
Full
past_due
Payment failed, 14-day grace
Full, with a banner
suspended
Grace ended, up to 30 days
Read-only for everything else; order import, reservation,
dispatch and stock push keep running so channels stay correct;
billing pages open
cancelled
Subscription ended, or 30 days suspended
Read-only and full export for 30 days. Stock push stops only after
the owner confirms "set channel stock to 0" (or is told it stays at its
last value)
deleted
After the retention period, or on request
Data hard-deleted, files included; the audit record of the deletion
is kept
GDPR: tenants can export all their data at any time
(CSV and JSON per module, plus one full export for leaving) and request
deletion. A data processing agreement covers the buyer personal data
from marketplace orders. Buyer addresses must be deleted or anonymised
after the period each marketplace's data policy requires (Amazon is
strict; see Integrations).
Platform admin (internal)
A separate, internal-only area for the Vervoro team:
Tenants: list tenants with their state, plan,
entitlements and usage.
Entitlements: grant or revoke manually (internal
tenants, goodwill credits).
Support access: time-limited, read-only by default,
visible to the tenant and fully audited.
Jobs: inspect each tenant's jobs and errors.
Platform-admin access uses a separate role. It never acts through a
tenant role.
Fixed decisions are the product owner's; changing
one needs a new entry in Decisions.
Recommended choices belong to the engineers: replace
one if you have a better reason, and record why.
Area
Choice
Status
Hosting
Cloudflare serverless (Workers, Queues, Workflows, Durable Objects,
R2, Hyperdrive). No VMs, no containers to operate
Fixed
Language
TypeScript, end to end
Fixed
Tenancy
One shared PostgreSQL database; tenant_id on every
tenant row; row-level security (RLS) enforced by the database
Fixed
Shape
A modular monolith in one repository. Modules declare themselves
through a manifest (below)
Fixed
Billing
Stripe
Fixed
Database host
Neon Postgres, London (aws-eu-west-2).
Launch plan now (0.25–2 compute units, scale-to-zero
off in production); Scale plan before the first
external tenant (30-day restore, SOC 2). Staging and production
connect through Hyperdrive with query caching disabled.
Chosen for branches that copy data, so every pull request tests its
migration on masked Seba-shaped data. Runner-up: PlanetScale Postgres.
See the
stack debate
Recommended
API
Hono + Zod; @hono/zod-openapi generates the OpenAPI
spec and a typed client
Recommended
UI
React 19 + Vite single-page app served as Workers Static Assets on
the same domain as the API. TanStack Router and Query. Tailwind 4 +
shadcn/ui (Radix)
Recommended
ORM / SQL
Drizzle: SQL-first, small bundle, RLS policies in the schema
Recommended
Auth
Better Auth with the organization plugin (organization = tenant),
stored in our Postgres
Recommended
Tests
Vitest with @cloudflare/vitest-pool-workers; Playwright
for end-to-end
Recommended
Errors and monitoring
Cloudflare Workers Logs + Workers Issues, a
browser-error relay and a 5-minute health cron (see Observability). No Sentry at launch;
Sentry Team is the fallback if Workers Issues (beta) disappoints
Recommended
Why a single-page app rather than Next.js: an ERP behind a login has
no SEO or server-rendering needs. seba-erp already works this way in
practice: 76 of 88 pages are client components and the UI makes 364
fetch('/api/…') calls. Plain React + Vite avoids running
Next.js through an adapter on Workers.
System overview
flowchart LR
B[Browser SPA] -->|same origin /api| W[web Worker: Hono API + static assets]
AI[Claude / ChatGPT] -->|MCP + OAuth| M[mcp Worker: Agents SDK]
MP[Marketplace and Stripe webhooks] --> W
W --> H[Hyperdrive] --> PG[(Neon Postgres, RLS)]
M --> H
W --> R2[(R2: tenant files)]
W -->|outbox relay| Q[Queues]
C[Cron Triggers] --> J[jobs Worker]
Q --> J
J --> WF[Workflows: long multi-step syncs]
J --> DO[Durable Objects: API rate limiters]
WF --> DO
DO --> X[Amazon SP-API / Ads / Vendor, eBay, Shopify, TikTok]
J --> H
W --> AIG[AI Gateway] --> LLM[Anthropic API]
Three Workers deploy from one repository and share packages:
Worker
Responsibility
web
The SPA's static files, /api/*, auth routes, inbound
webhooks (verify, store to inbox, enqueue)
Suggested layout: apps/web, apps/jobs,
apps/mcp, packages/db (schema, migrations,
withTenant), packages/platform (auth, tenancy,
RBAC, entitlements, audit, module registry),
modules/<key> (one folder per module, with server and
UI).
Tenancy
Tenant comes only from the server. It is the
session's active organization, a job message signed by our own code, or
an MCP token. A tenant_id in a request body, query or
header is never trusted.
Every tenant table has
tenant_id uuid not null. Uniqueness includes it:
unique (tenant_id, sku),
unique (tenant_id, connection_id, native_order_id). Foreign
keys between tenant tables include tenant_id (composite
foreign keys), so a row can never point at another tenant's row.
RLS is enabled and forced on every tenant table,
with policy
tenant_id = nullif(current_setting('app.tenant_id', true), '')::uuid
for both USING and WITH CHECK. Written this
way, a missing or empty tenant setting matches no rows instead of
raising an error (rule T2).
A CI catalogue check fails the build if any table
outside the platform and auth schemas lacks
tenant_id, row security, or forced row security.
Drizzle can declare policies; the FORCE ROW LEVEL SECURITY,
policies and grants are written as reviewed SQL in the migration.
The runtime database role does not own the tables and has neither
BYPASSRLS nor superuser.
Migrations run as a separate owner role.
Set the tenant context inside every transaction:select set_config('app.tenant_id', $1, true). Hyperdrive
pools connections per transaction and resets SET when a
connection returns to the pool, so a session-level SET does
not survive. All database access goes through one helper,
withTenant(tenantId, tx => …); code outside
packages/db never gets a raw connection.
The same helper sets lock_timeout 5 s and
statement_timeout 30 s with
set_config(…, true).
The runtime role has an
idle_in_transaction_session_timeout.
A lint rule forbids session SET outside
packages/db.
Hyperdrive query caching is off on every
configuration (--caching-disabled). The RLS filter lives
inside the policy, not in the SQL text, so every tenant sends identical
queries. A cached result could be served to the wrong tenant.
Belt and braces: every query also filters
tenant_id = $1 explicitly.
The isolation suite runs the same SELECT as two tenants back to
back.
Lock discipline for stock: lock all needed batch
rows in one statement, ordered by id, then write in one batch. Keep a
stock transaction to a few round trips, and log transaction time and
lock waits from day one.
Platform tables (tenants, plans, Stripe events,
platform admins) live in a separate platform schema without
tenant RLS. Only platform code can reach them.
Cross-tenant work has exactly one door: a named
database role (vervoro_platform) used only through one
helper, withPlatform(reason, tx => …), which writes an
audit row for every use. It reads the platform and
auth schemas and the minimum needed to fan out work (which
tenants and connections exist, which tenant a webhook belongs to). It is
used by: the cron dispatcher, webhook routing, the health cron, the
retention job and platform-admin screens. No code uses the owner
role or BYPASSRLS at runtime. Tenant work always
continues inside withTenant.
Jobs: every queue message and Workflow input
carries tenant_id; the consumer opens
withTenant with it.
Files: R2 keys start
tenants/{tenant_id}/. Files are served only through an
authorized API route or a short-lived signed URL. Nothing is
public.
Caches: Workers isolates are many and short-lived,
so in-process caches (like seba-erp's globalThis access
cache) are unreliable. Use KV or the database, and always include
tenant_id in a cache key.
Isolation test suite in CI. Two tenants with
overlapping SKUs and order numbers. Every API route, export, file route,
MCP tool and job is called as tenant B against tenant A's ids, and must
return nothing or 404. It also runs the same query twice in a row as two
tenants, and checks idempotency keys and invitations (below) can't cross
tenants.
Modules
Each module exports a manifest. The platform reads it to mount
routes, build the navigation, register jobs, list permissions and
enforce entitlements. This replaces seba-erp's path-prefix list
(lib/modules.ts).
Entitlement and enablement are checked by
middleware on the module's mount point, so a route cannot forget them.
Checks are always by module key or limit, never by plan
name. Plans and add-ons are data in the plan catalogue (Commercial
model), so moving a module between plans or selling it on its own
needs no code change. Scheduled jobs check the same gates before they
run per tenant.
Own tables only. A module writes only its own
tables (prefix them, e.g. ads_campaign). It reads other
modules through their exported service functions, not their tables.
Stock changes only through the inventory service
(inventory.receive, reserve,
release, dispatch, adjust,
transfer). No other module writes stock tables.
Cross-module reactions use domain events. Inside a
transaction, write a row to the outbox (e.g.
order.dispatched); subscribers run from the queue
afterwards.
Connectors implement one interface:importOrders, pushStock,
listListings, notifyDispatch,
verifyWebhook, refreshToken. Capabilities a
channel lacks are declared, not faked.
Every connection has a mode: shadow or
active. In shadow it reads (orders,
listings, Vendor POs, FBA shipments) but never sends:
Each would-be stock push, dispatch notification or Vendor
confirmation is written to the outbox with status
suppressed, a final status the relay never
sends, kept for inspection.
Switching a connection to active sends
nothing from its past; a test proves it. The switch is
an audited action that requires the unmanaged-listings check (rule C6)
and starts with a full push computed fresh.
New connections start in shadow, for every tenant's
onboarding and migration (Customer
migration).
A tenant with any connection in shadow sends no
email and no documents (POs to suppliers, invoices, alerts to
outside addresses); they are stored as suppressed too.
Training: staff practise in staging or the demo
tenant, never on real stock in production.
Adding a new sales
channel
New platforms (Walmart, Target Plus and others for the US; Etsy,
Kaufland, OTTO and others in Europe) must be addable without
changing core code:
A channel is a connector module. It is a folder
under modules/ with a manifest, its own settings screen,
jobs and rate-limit settings. Channels are included in every plan
(D-037), so a connector has no Stripe price of its own. Channel keys are
strings registered by connector modules, never a hard-coded
enum. seba-erp's OrderChannel enum is the pattern
to avoid.
Canonical models in core: order, order line,
listing, SKU mapping, stock target, shipment notification, buyer
address. Connectors translate to and from these; core never sees a
channel's raw format, except as a stored original payload for
support.
Capabilities are declared: e.g.
orders.import, stock.push,
dispatch.notify, listings.read,
cancellation.read, returns.read. The UI and
jobs read them, so a channel without a feature shows that clearly.
Per-channel facts are data: currency, marketplaces,
address format, tax fields, rate limits, polling interval.
Conformance test kit: every connector must pass the
same shared tests (idempotent import, unmapped SKUs, multipliers,
merchant-fulfilled-only push, stock push only on change, dispatch
notification retries, token refresh) against recorded fixtures before it
is enabled. Before cutover, record 5–10 real orders per channel from
Seba's accounts (masked) as the first fixtures.
Template: keep
modules/connector-template up to date, so a new channel
starts from a working skeleton.
RBAC
Permissions are strings,
module.resource.action, declared by modules. A user's
permissions come from their role in the tenant. The full catalogue, the
role matrix, warehouse scoping and the data model are in Permissions and roles.
Built-in roles (Owner, Admin, Manager, Warehouse,
Accountant, Staff) and what each gets are defined only
in Permissions and roles. A custom role
editor comes later; the data model already supports it.
Sensitive data has its own permissions: cost prices
and margins (finance.costs.read), buyer personal data
(orders.buyer_pii.read) and exports
(data.export).
Warehouse scope: a user can be limited to some
warehouses; see Permissions
and roles.
Checked on the server for every request, with
requirePermission("orders.dispatch"). The UI hides what a
user can't do; it is never the security boundary.
First-user rule: the user who creates a tenant is
its Owner. There is never an "everyone is admin while no admin exists"
fallback; seba-erp has one in lib/access.ts, and with many
tenants it would be an escalation bug.
Audit log: every mutation records tenant, actor,
action, entity, a before/after summary, and the request or job id.
Jobs and integrations
Cron Triggers decide what is due. They
enqueue one message per tenant connection and job (e.g.
{tenant, connection, job: "amazon.orders"}) and never do
the work themselves.
Queue consumers do short work: import a page of
orders, push changed SKUs.
Workflows do multi-step or waiting work: an FBA
inbound plan, requesting an Ads report then polling until it is ready,
reading a settlement. Each step retries on its own.
Schedules: module manifests declare
schedules, but Cron Triggers are fixed in the Worker's
config. So the jobs Worker has one
dispatcher cron (every minute) that reads every module's schedules and
enqueues what is due. Never add a Cron Trigger per module.
Rate limits: one Durable Object per
(connector, account, operation) acts as a token bucket, set
to that operation's published rate and burst. Every call to a
marketplace asks it first. On HTTP 429, wait the provider's
Retry-After if it sends one, otherwise the operation's
restore time (Integrations:
Amazon). While seba-erp still runs amazon-settlements
on the same Amazon app and seller account, it uses part of the same
quota; leave headroom for it.
Amazon call counting: every SP-API GET is counted
per tenant (Analytics Engine), as required by Integrations.
One writer per channel account: a
ChannelAccountWriter Durable Object per connection holds
the sync leases. Only one job at a time can push stock or import orders
for that account (rule E6).
Worker placement: placement hints apply only to
HTTP (fetch) handlers, not to queue, cron or Workflow handlers. Keep
stock transactions to a few round trips. Only if traces show distant job
consumers holding locks, move the inventory service into a small Worker
placed near the database and call it through a service binding.
Outbound changes (stock push, dispatch
notification, Vendor confirmation) follow the outbox pattern. The domain
change and the outbox row commit in one transaction; the relay sends the
row to a queue; the consumer is idempotent.
Sync state: each connection keeps a watermark per
sync. Overlapping time windows are safe because imports are
idempotent.
Visible to users: jobs view per connection, with
what ran, results, errors and next run.
API conventions
Routes: REST-style JSON under
/api/<module>/…. The OpenAPI spec is generated from
the Zod schemas and used to generate the typed UI client.
Errors: one shape everywhere,
{ "error": { "code": "STOCK_INSUFFICIENT", "message": "…", "details": {…} } }.
Codes are stable strings the UI can map to translated messages; HTTP
status follows meaning (400, 401, 403, 404, 409 conflict/stale version,
422 rule violation).
Lists: cursor pagination
(?cursor=…&limit=50, maximum 200), server-side
filtering and sorting. Never return a whole table.
Writes that must not happen twice (create order,
dispatch, receive, confirm Vendor PO, apply proposal) accept an
Idempotency-Key header. Keys are stored per tenant
and route with a hash of the request body and the first
response, and expire after 24 hours. A repeat with the same body returns
the first response; the same key with a different body returns 422.
Concurrency: edits send the record's
version; a stale version returns 409.
Money and quantities travel as strings for money
("12.3400") with a currency code, and integers for
quantities.
UI conventions
No design system yet: use shadcn/ui defaults with
one accent colour, and keep it consistent. Branding can come later.
Mobile-first where practical; every screen works on a
phone (D-063). Seba runs much of its day from phones.
Designed for the phone first: scan, receiving,
picking, packing, dispatch, tasks, returns, attendance, the order list
and order page, Vendor PO review and approval, and the product
page.
Usable on a phone, richer on a desktop: large
tables, reports, imports, settings. On a phone they show fewer columns
or a card list. They must work there, not be perfect.
Dense, sortable, filterable tables and keyboard shortcuts on
desktop.
Barcode scanning works two ways everywhere a code
is entered: a hardware scanner (it types like a keyboard) and the
phone camera. The camera scanner reads EAN, UPC, Code
128 and QR, prefers the rear camera and ignores the same code repeated
within 3 seconds. seba-erp: components/BarcodeScanner.tsx
(ZXing).
Screens rebuild seba-erp's behaviour, not its
layout.SCREENS.md lists every
seba-erp page with its module and priority.
English only in V1, with all text in message files
so translations can be added later.
Clear states: loading, empty, error and "saved" on
every screen. A stale-version conflict tells the user who changed
what.
Data conventions
IDs: UUID v7.
Time:timestamptz in UTC. Business
dates (best-before, leave days) are date, interpreted in
the tenant's timezone.
Money:numeric(14,4) plus a currency
code on the same row. Never add amounts across currencies; never convert
silently.
Quantities: integers in base units. Packs and boxes
are multipliers on top of base units, never a separate count.
Rows: every row has created_at,
updated_at and created_by. Edits use
optimistic concurrency (version column) wherever two people
could change the same record.
They are never stored per tenant. seba-erp kept them in its settings
row next to tenant tokens.
Tenant tokens: refresh and access tokens are
encrypted with AES-GCM via WebCrypto. The key comes from Secrets Store
and carries a key version, so keys can be rotated.
Sessions: HttpOnly, Secure, SameSite=Lax cookies on
the app domain. State-changing requests require a CSRF token or an
Origin check.
Sign-in: multi-factor authentication (Better Auth
twoFactor plugin) is required for every
user of a tenant that has an Amazon connection (Amazon's
data-protection rules), and offered to everyone else.
Invitations: an invitation or a Google sign-in
joins a tenant only for a verified email that exactly
matches the invited address.
Browser hardening:frame-ancestors 'none' on the app and the MCP consent page;
a strict Content-Security-Policy.
Abuse protection: Turnstile on sign-up and password
reset; rate limiting on authentication routes.
Webhooks: every inbound webhook is
signature-verified and stored before it is processed.
Server-side fetches of user-supplied URLs (e.g. AI
images) allow only public https and block private addresses.
Logs include tenant_id and the request
id, and never tokens or buyer addresses.
AI and MCP
Who pays for tokens: MCP tool calls are driven by
the customer's own assistant (Claude, ChatGPT), which pays for the
tokens. Vervoro pays only for model calls made inside Vervoro.
Gateway: every model call Vervoro makes goes
through AI Gateway, which gives per-tenant logging, cost tracking and
rate limits. The default provider is Anthropic. Usage is metered per
tenant (Analytics Engine) for billing.
MCP server:
Built with the Agents SDK plus workers-oauth-provider,
which stores its grants in Workers KV.
Served at /mcp* on the app's own host, so the OAuth
consent step can see the user's Better Auth session. The consent screen
names the client (e.g. Claude) and the tenant being connected; if the
user belongs to several tenants they choose one.
A token belongs to one user in one tenant, expires (access 1 hour,
refresh 30 days), and can be revoked.
Each tool checks that user's permissions and the ai
entitlement on every call.
Tool output passes the same field filter as the
API: no cost fields without finance.costs.read,
and never buyer personal data through MCP. Bulk reads
need data.export. A test runs every tool with a
Warehouse-role token.
Tool kinds: read tools return data; propose tools
create a proposal record. Proposals apply only after a person approves
them, with the target re-checked first. See Domain rules.
Files, PDFs, email
Files: R2, EU jurisdiction. Uploaded files are
served as downloads
(Content-Disposition: attachment,
X-Content-Type-Options: nosniff, a sandbox CSP), never
rendered inline from the app's origin, so an uploaded SVG or HTML file
can't run script in a user's session. File types are checked by content,
not by name or declared type.
PDFs: generated with pdf-lib (pure
JavaScript; seba-erp's pdfkit needs file-system fonts).
Barcode drawing comes from seba-erp's own Code 128 encoder
(lib/barcode128.ts). Label sizes 15 × 10 cm and A4. Browser
Rendering can do HTML→PDF if a template is easier in HTML.
Reading PDFs (supplier documents):
unpdf instead of pdf-parse.
Excel: test exceljs under
nodejs_compat. For large workbooks, stream them, or write
CSV, which is often enough.
Email: Cloudflare Email Service for sending (POs to
suppliers, invitations, alerts). Inbound email (for a future accounting
inbox) uses Email Routing plus an Email Worker, not IMAP polling.
Workers limits
and how we design for them
Checked against Cloudflare's docs on 2026-10-06, Workers Paid plan.
Re-check before relying on a number.
Limit
Value
Design response
Memory per isolate
128 MB
Stream large files to and from R2. Paginate every import. Never load
a whole catalogue or report into memory
CPU per request
30 s by default, up to 5 min configurable
Keep requests short; heavy work goes to queues and Workflows
Cron and queue consumer duration
Up to 15 min
Process one page per message and re-enqueue
Workflow step CPU
30 s by default, up to 5 min; unlimited wall clock; sleeps up to 1
year
Use for report polling and multi-step marketplace flows
Simultaneous outbound connections
6 per invocation
Fan out through queues, not Promise.all over 100
SKUs
Hyperdrive pooling
Per transaction; SET is reset
Use set_config(…, true) in every transaction (see
Tenancy)
Escape hatch
Containers (Workers Paid)
Only if a job proves it cannot fit the above
Environments and delivery
Environments:
Local:wrangler dev with a local
Postgres or a personal Neon branch.
Preview: one per pull request, on its own Neon
branch.
Branched only from the masked seed: import
templates with personal columns replaced by fake but consistent values,
or synthetic data; no marketplace tokens. CI refuses any other parent
branch.
The branch expires and is deleted when the PR closes.
Preview Workers connect directly to their branch
with pg, not through Hyperdrive. Hyperdrive allows 25
configurations per account, and previews need no pooling.
Same db(env, region) code path, a fresh client per
request or job, and the runtime role (never the owner).
Staging: always through Hyperdrive (caching off),
on an unmasked copy of the data. This is where pooling behaviour is
proven: CI runs the isolation suite and a concurrent reserve/dispatch
test here before every production deploy.
Staging must never reach a real marketplace or
customer. It has its own token-encryption key (so production
tokens can't be decrypted there), the copy is made with all marketplace
tokens removed, and its outbox relay runs in dry-run:
it logs what it would send and sends nothing. Outbound email goes to a
test inbox. The buyer-data anonymiser runs on staging too, so its copy
follows the same 30-day rule as production.
Production.
Pipeline:
CI runs typecheck, lint, unit, integration (real Postgres with RLS)
and the isolation suite.
Migrations run before deploy, over Neon's direct connection as the
owner role, never through Hyperdrive. A migration must work with both
the old and the new code.
Backups:
Neon point-in-time restore: 7 days on Launch, 30 on Scale.
A nightly pg_dump -Fc from a scheduled
GitHub Action to R2 (EU). It is provider-independent, so it is our way
off Neon if ever needed. The dump runs as a read-only
database role, is encrypted before it leaves the runner
(e.g. age with a key held only by the owner and engineers),
and is kept for 14 days, so buyer data in backups
doesn't outlive Amazon's limit by much. GitHub is therefore a
sub-processor (Operations).
The restore rehearsal before cutover restores that dump; write down
how long it took.
Regions (US later)
US customers get their own database cell, not a
shared global database.
Day 1 (EU only):
tenant.region column.
A db(env, region) factory that picks the region's
Hyperdrive binding.
Better Auth tables in their own auth schema.
A global directory that needs only
email_hash → region.
When a signed contract needs it:
A second Neon project in a US region (a Neon project's region can't
be changed later), plus a second Hyperdrive configuration.
R2 and Durable Objects in their us jurisdictions.
Each tenant is pinned to one region.
Residency wording for contracts and the DPA:
Business data, including orders and buyer personal data, stays in
the tenant's region.
Account identity (email, name), the tenant directory and billing
data are global, held in the EU cell. Stripe processes billing data in
the US anyway.
Queues, Workflows and logs carry ids, never
personal data. Cloudflare documents no jurisdiction control for
them.
EU customers: London is acceptable, because the
EU's UK adequacy decision is renewed until 27 Dec 2031 (verify with
counsel). Add Frankfurt only if a contract requires storage inside the
EU.
Observability
Workers Logs (7-day retention) for structured
logs. Every line carries tenant_id and the request or job
id, never tokens or buyer data. Sample high-volume logs.
Security logs kept 12 months (Amazon requires at
least 12 months): sign-ins, permission changes, support access, token
use and audit events are shipped with Logpush to R2
(EU) and kept for 12 months, without buyer data.
Workers Issues (open beta) groups exceptions,
failed invocations and 5xx responses. Its automations post to chat, an
incident tool or a webhook.
Browser errors: the SPA sends
window.onerror and unhandledrejection to
/api/client-errors, which logs them as errors so Issues
picks them up.
Health cron every 5 minutes. The failures that
hurt are silent, so it checks:
order-import watermark age per connection;
age of the oldest outbox row;
failed jobs;
drift between the stock ledger and stock balances (rule S1).
Anything wrong is logged as a tagged error that an Issues automation
turns into a page.
Fallback: Sentry Team, if Issues misses things
in the first weeks after cutover.
These rules cannot be broken. Each one needs at least one automated
test, named with the rule id (e.g.
S7 applies pack multiplier). Purchasing rules are
PU, so they can't be confused with the P0/P1/P2 priorities.
Where seba-erp learned a rule the hard way, its file is named so you can
see the original bug.
Tenancy
T1. The tenant comes from the server (session,
signed job message, MCP token), never from the client.
T2. Every tenant row has tenant_id,
and RLS enforces it. A missing tenant context returns no rows; it never
returns all rows.
T3. Identifiers that are unique in the real world
are unique per tenant: SKU, batch number, native order id per
connection.
T4. Jobs, files, caches, exports and MCP tools are
tenant-scoped the same way API routes are.
Stock
S1. Ledger first. Every physical stock change is an
immutable movement (received, transferred,
dispatched, adjusted,
repacked_in/out, returned,
written_off) with quantity, batch, location, status,
reason, actor and source document. Stock balances are updated in the
same transaction and must always equal the sum of
movements. Reserving and releasing are allocations (S2), not movements.
A nightly job checks this and alerts on any difference.
S2. One reservation implementation. Reserving and
releasing stock is one function in the inventory service, used by every
channel, manual orders, Vendor and FBA. A reservation is an
allocation row linking an order line to a batch and
quantity, not just a counter. seba-erp: lib/order-stock.ts,
where per-channel copies drifted and left reserved quantities
stranded.
S3. Physical ≠ sellable. Channels always receive
sellable, never physical. seba-erp:
lib/stock-availability.ts, where Shopify was pushed
physical stock and oversold.
Product sellable = physical saleable stock in the
warehouses serving that channel account − reserved − safety buffer,
counting only batches that are not expired (and meet
any minimum shelf life set for the channel).
Kit sellable = the minimum over its components of
⌊component sellable ÷ component quantity per kit⌋.
Listing quantity pushed = ⌊sellable ÷ the mapping's
multiplier⌋ (a 6-pack listing on a product with 20 sellable pushes
3).
S4. Never negative, never clamped. A movement that
would take a balance below zero fails with an error. Never hide it by
clamping to zero.
S5. Physical stock leaves only at dispatch
confirmation. Labels, pick lists, packing lists, tasks and
channel "shipped" messages do not move stock.
S6. FEFO by default. Allocation takes eligible
batches with the earliest best-before date first. A manual override is
allowed and audited. Expired batches are not allocated.
S7. Base units everywhere. Packs and boxes are
multipliers that are always applied. seba-erp:
lib/fba-stock.ts, where shipping five 10-packs deducted 5
instead of 50.
S8. Kits vs packs. A bundle/kit has no stock of its
own; it reserves and dispatches its components. A physical pack has its
own stock; dispatching it does not consume components again. A repack
job converts components into packs with paired movements.
S9. Returns go to quarantine. Returned goods are
not sellable until a person inspects them as saleable, damaged or
written off. A refund never restocks anything, and a restock never
refunds anything.
S10. Moving stock to FBA is a transfer, not a sale.
It creates no order and is never "confirmed" as a sale. Stock is
reserved against the FBA shipment once it is ready to
pack (so it stops being pushed to channels) and deducted at
handover. Each FBA shipment is recorded once, keyed on Amazon's
shipmentId. A shipment created outside Vervoro (Seller
Central's Send to Amazon) is recorded in Vervoro the same way, and a
daily check lists Amazon shipments with no Vervoro record.
S11. Receiving creates a batch with landed cost
(provisional until all charges arrive). Batches without a supplier lot
get YYYYMMDD-NN references.
Orders
O1. One canonical order per native order. The key
is (tenant, connection, native_order_id). Re-importing
updates the order; it never duplicates it. Two accounts that use the
same order number are two different orders.
O2. An order line whose SKU is unmapped is imported
and flagged, reserves nothing and appears in "unmatched SKUs". Mapping
it later triggers reservation.
O3. Shortage: reserve what is available and show
what is missing. Never cancel automatically and never assume stock will
arrive.
O4. Pre-dispatch edits: a native quantity decrease
releases stock; an increase tries to reserve more.
O5. Cancellation: a cancellation request
is not a cancellation. Reservations stay until cancellation is
confirmed, then are released once.
O6. FBA orders (Amazon-fulfilled) are recorded for
reporting and dashboards and never reserve own-warehouse stock. Every
imported order stores its fulfilment channel (merchant
or Amazon) from the marketplace data. Both values are matched positively
(Amazon: AFN = Amazon, MFN = merchant); a
missing or unknown value is not reserved until it is known. seba-erp:
lib/amazon/order-import.ts.
O7. An order total is the sum of its lines, in the
order's currency. Marketplace fees are not part of a sale.
O8. Dispatch is recorded once. Retries, tracking
updates or channel messages never create a second dispatch.
O9. One warehouse per order. An order reserves and
ships from one warehouse: its channel account's default warehouse, which
an authorised user can change before picking (reservations move with
it). The stock pushed to a channel account is the sellable stock of the
warehouses that serve it. Splitting one order across warehouses and
automatic routing rules are later scope.
External systems
E1. No network calls inside a database transaction,
and never while holding stock locks.
E2. Outbox. Anything that must reach a marketplace
(stock push, dispatch notification, Vendor confirmation, ads change) is
written to the outbox in the same transaction as the change. It is sent
afterwards by an idempotent job with a stable operation id.
E3. A timeout is "unknown", not "failed". For
anything that is not naturally idempotent, read the native state back
before retrying. Never blindly resend a confirmation, a cancellation or
a campaign creation.
E4. Stock push sends the latest
absolute quantity, coalescing multiple changes, and
only for SKUs whose sellable quantity changed since the last confirmed
push. seba-erp: lib/stock-push.ts. Pushing every SKU every
run cost 3 calls and 700 ms per SKU on Amazon and drained Shopify's rate
limit.
E5. Every marketplace call goes through the rate
limiter for its account and operation. HTTP 429 means wait (for
Retry-After if sent, otherwise the operation's restore
time), then retry; it does not mean the SKU is broken. seba-erp:
lib/amazon/spapi.ts.
E6. One stock writer per channel account. Only one
system, and one job, may push stock to a given account at any time.
While a customer migrates, the old system is the writer and Vervoro's
connections stay in shadow mode; at cutover the roles
swap.
E7. A queued request or an HTTP 200 is not proof
that the channel applied the change. Reconcile periodically by reading
native quantities and orders back.
Channels and accounts
C1. A tenant can connect several accounts of the
same connector. Identity is the native seller, shop or store id;
reconnecting the same account keeps its history, mappings and
watermarks.
C2. Mapping is per connection: (connection,
channel SKU) → product × multiplier, fulfilment type. The
multiplier is how many base units one channel unit contains (a 6-pack
listing = 6); order quantities are converted with it when the order is
imported, in one place for every channel. Fulfilment type is merchant or
Amazon (FBA). The channel's product reference (ASIN, eBay item id,
Shopify variant id) groups several SKUs of one listing.
C3. Only active listings are offered for
mapping. seba-erp: lib/listing-sources.ts.
C4. A tenant only sees channels it actually uses;
don't show empty rows for connectors it never connected. seba-erp:
lib/selling-channels.ts.
C5. Stock is pushed only to merchant-fulfilled
listings. Never push a quantity to an FBA SKU: on Amazon this
can switch the listing to merchant-fulfilled. seba-erp:
lib/stock-push.ts.
C6. Unmanaged listings are not left selling. Before
stock push is switched on for a connection, every active
merchant-fulfilled listing is read back. Any listing with quantity above
zero that Vervoro doesn't push must be mapped, or set to zero, or
explicitly signed off.
Vendor Central
V1. Every PO confirmation needs explicit user
approval, even when stock covers it. The approved quantities and prices
are what get sent; a later stock change never silently alters them.
V2. Confirmed unit cost ≤ PO cost × (1 +
tolerance), rounded down to the penny. Tolerance is a
tenant setting, defaulting to 2%. seba-erp:
lib/vendor-pricing.ts.
V3. Rejected quantities create no reservation and
no backorder. The accepted quantity ships from one warehouse, in one
shipment.
V4. Approval creates a Vendor-channel order that
reserves through the normal inventory service (S2). seba-erp:
lib/vendor-stock.ts.
FBA, purchasing, ads
F1. Inbound plans follow Amazon's per-SKU prep and
label rules, never a blanket default. Boxes over 15 kg get the team-lift
warning.
PU1. A supplier document is accepted only if its
lines reproduce its printed totals exactly. Otherwise a person keys it
in.
PU2. An unknown supplier product code stops the PO
until a person maps it; the mapping is remembered per supplier.
A1. The ads engine acts only on ad groups that
advertise a single ASIN, and only after a person approves the
proposal.
AI
AI1. AI never changes data directly. It creates a
proposal with the exact content and the state of the
target it was based on.
AI2. Applying a proposal re-checks that the target
still matches that state. If someone changed it in the meantime, the
proposal is refused and the user is told why. seba-erp:
lib/amazon/listing-edit.ts.
AI3. An MCP token acts as one user in one tenant,
with that user's permissions. It can be revoked at any time and never
exposes marketplace credentials.
AI4. MCP never returns buyer personal data, and its
output goes through the same field filter as the API (no costs without
finance.costs.read). Bulk reads need
data.export.
Glossary
Term
Meaning
Tenant
One customer company; the data and billing boundary
Connector / connection
A channel integration type (e.g. Shopify) / one connected account of
it
Batch
Stock received together, with lot, dates and cost. Where it is, and
in what state, is held in its stock balance rows
Movement
One immutable stock ledger entry
Allocation / reservation
Stock bound to an order line from a specific batch
Sellable
Physical saleable stock − reserved − buffer; what channels
receive
Dispatch
The confirmed physical handover; the only thing that deducts stock
for a sale
Outbox
Rows written in a domain transaction, delivered afterwards to
external systems
Entitlement
A module or quantity the tenant has paid for or been granted
Proposal
A change suggested by AI or the ads engine, waiting for human
approval
Every connector implements the connector interface in Architecture: Modules and follows
Domain rules: External
systems. seba-erp shows what works today against Seba's real
accounts; read it before writing a connector.
Statements marked verify come from seba-erp's notes
or from memory. Check them against the provider's current documentation
before you depend on them.
Approvals to start now
These take weeks and are outside the engineers' control. They don't
block Seba: until Vervoro's apps are approved, Seba connects through
its own existing Amazon and eBay apps, plus a Shopify
custom-distribution app and a TikTok self-developed app made for Vervoro
(see below
and the Day-1
checklist). They do block every external customer.
Item
Owner
Status
Legal entity that owns the developer registrations: the new Vervoro
company (D-046)
Product owner
Company being opened
Amazon SP-API developer profile and a public app
with Seller roles, Vendor roles, and restricted roles for buyer PII
(Amazon's data-protection review)
Product owner
Not started; needs the Vervoro company
Amazon Ads API developer registration (separate from SP-API)
Product owner
Later (Ads module)
eBay production keyset, including the mandatory
marketplace-account-deletion notification endpoint (built by the
engineers first)
Product owner + engineers
Not started
Shopify Partner account; custom-distribution apps per merchant now,
public App Store review later (D-042)
Product owner
Not started
TikTok Shop partner app review
Product owner
Not started
Stripe account, Stripe Tax, products and prices
Product owner
Not started (P2)
Domain, Cloudflare account on Workers Paid, Neon (London), GitHub
organisation
Product owner
Before day 1
Running
alongside the old system: separate authorisations
While a customer migrates, the old system and Vervoro (in shadow
mode) read the same accounts at the same time; for Seba that is
seba-erp. Each system holds its own tokens, so a token
refresh in one can't invalidate the other and a rollback never needs a
re-login. Where both use the same app, its app-level credentials are
still shared; that is noted below.
Channel
Vervoro's own authorisation
Still shared, and what to do
Amazon Seller and Vendor
A second self-authorisation of Seba's existing apps (Amazon allows
up to 10 per app). A new self-authorisation doesn't revoke earlier
refresh tokens, so each system has its own
The app's client secret, which Amazon requires to
be rotated every 180 days, and revoking the app in Seller Central cuts
off both systems. Agree one rotation runbook for both, and check before
cutover that the next rotation doesn't fall in the first weeks after
it
eBay
Vervoro's own RuName on Seba's eBay app keyset, and
a separate consent
Verify that the second consent leaves seba-erp's
token working, on a test account first
Shopify
Vervoro's own custom-distribution app, created in
Vervoro's Shopify Partner account (needed by about day 6). Build token
refresh in from the start. Verify whether protected
customer data must still be requested
Nothing. Sharing seba-erp's custom-app token is ruled out: it
carries write scopes
TikTok Shop
A second self-developed app for Vervoro.
Re-authorising the same app reportedly invalidates the earlier tokens,
so the two systems must never use the same app
Nothing
Both systems use the same Amazon seller account and app, so they
share its rate limits: Vervoro polls less often in shadow mode, imports
FBA orders by report, and backs off automatically when the shared quota
is under pressure.
Amazon Seller
(amazon-seller): SP-API
Auth: Login with Amazon OAuth (website
authorization workflow); one refresh token per seller account and
region. seba-erp has app/api/amazon/login and
callback built but never run against a public app.
App type and authorisation limits (Amazon's
authorisation-limits page, checked 2026-10-06):
App type
Who can authorise it
Limit
Private
Only your own organisation, by self-authorisation
10 self-authorisations
Public, unlisted
Other sellers and vendors through OAuth
25 sellers (vendors unlimited), plus 10
self-authorisations
Public, listed on the Appstore
Anyone through OAuth
Unlimited
A private ("developer mode") app cannot serve other
companies. It is for your own organisation only. Vervoro's
customers are other companies, so customers need a public app from the
start.
Public, unlisted covers the plan of 10–20
customers. Register Vervoro as a public developer, keep the app
unlisted, and onboard up to 25 sellers through OAuth. List on the
Appstore before seller number 25.
The review is the long-lead item. Public developer
registration (and restricted roles for buyer PII) goes through Amazon's
review whether the app is listed or not, so start it now.
Bridge for Seba: until Vervoro's public app is
approved, Seba UK's tenant can connect through Seba's existing private
app by self-authorisation (Seba's own accounts). Store that app's
credentials encrypted on the connection, as with Shopify custom apps.
Switch the connection to Vervoro's app once it is approved.
Used by Seba:
Orders, FBM and FBA, on separate schedules.
Listings Items API for FBM stock push.
Catalog and listing text edits.
A+ content (read and submit; the AI module).
Product pricing (advanced reports).
Finances API (payout profitability; accounting later).
FBA orders at volume: Seba UK alone has about
850 FBA orders a day. Fetching items order by order costs one GET call
per order. Import FBA orders in bulk from Amazon's order reports instead
(e.g. every few hours), and keep per-order API calls for FBM orders,
which must arrive quickly.
Buyer data: without restricted roles and a
Restricted Data Token, an order carries only city, postcode and country,
with no name or street. Plan for both states. Amazon's Data Protection
Policy limits how long buyer data may be kept; implement the retention
it requires.
Fulfilment channel (AFN / MFN): each order's
FulfillmentChannel is AFN (Amazon-fulfilled,
FBA) or MFN (merchant-fulfilled). Test both positively:
treating "not AFN" as merchant-fulfilled would reserve warehouse stock
for orders Amazon ships itself. An order with a missing or unknown value
waits until Amazon reports it (rule O6). seba-erp:
lib/amazon/order-import.ts.
Rate limits:
Limits apply per selling partner and application, so tenants don't
share a quota. seba-erp relies on this; only the Amazon Ads API is
shared enough to need staggering (lib/scheduler.ts).
Amazon rarely sends Retry-After on a
429. Wait for the operation's restore time instead. From seba-erp
(lib/amazon/spapi.ts and callers): getOrders
restores about one request a minute, so its retries wait 65 seconds;
Finances calls retry after 3–5 seconds; 10 seconds suits most other
operations.
Check each operation's published rate and burst in Amazon's docs and
set the Durable Object limiter to them.
Polling intervals Seba runs today (seba-erp
lib/scheduler.ts; a starting point, not a rule):
Job
Every
Amazon FBM orders; eBay and TikTok orders
30 min
Amazon, eBay, Shopify and TikTok stock push; Shopify orders
FBM orders use a watermark ("changed since the last run"), so a
routine run fetches only the few orders that moved.
Cost of stock push: about 3 calls and 700 ms per
SKU, so only push changed SKUs (rule E4).
Marketplaces (UK, DE, …) are destinations under
one seller account, not separate accounts.
API fees (watch this):
In November 2025 Amazon announced fees for third-party developers: a
$1,400 annual fee, plus monthly tiers on GET calls only. Basic: 2.5M
calls free. Pro: $1,000/mo for 25M. Plus: $10,000/mo for 250M. Overage:
$0.40 per 1,000 calls.
On 12 May 2026 Amazon withdrew them "at this time", so today SP-API
is free. They may come back, so design to keep GET calls low.
How to keep GET calls low:
Use the Notifications API (e.g. order-change notifications) instead
of polling where possible. Amazon delivers notifications only to an AWS
SQS queue or EventBridge, so this needs a small AWS account and a bridge
into Cloudflare Queues. For the first version, polling is
fine at Seba's volume; add notifications later.
Use reports instead of many single-item GETs.
Only re-read what changed.
Measure GET calls per tenant from day one
(Analytics Engine), so a fee change can be priced into plans.
Don't split tenants across several developer accounts or
apps to stay under a free tier. Each app needs its own Amazon
review and its own seller authorisations. It would probably breach
Amazon's developer policies, and each account would pay the annual fee
anyway.
Amazon Vendor
(vendor): SP-API vendor roles
Account: a separate authorisation and refresh token
per Vendor account (e.g. Vendor UK, Vendor DE), identified by its vendor
code (party id, usually 5 characters).
Connecting a Vendor account differs from Seller
(seba-erp: app/(dashboard)/settings/amazon-vendor,
summarised here):
It is often a separate LWA app from the seller one,
with its own client id and secret. A refresh token belongs to one app;
exchanging it with another app's credentials fails with
invalid_grant. Store the app credentials with the
connection.
Authorise from Vendor Central, not Seller Central.
A seller account holds no vendor entity, so Seller Central's app list
refuses. The consent link is
https://vendorcentral.amazon.co.uk/apps/authorize/consent?application_id=<amzn1.sp.solution…>&version=beta,
opened while signed in to Vendor Central.
Amazon returns an spapi_oauth_code to
the app's OAuth redirect URI. It expires within minutes, so the callback
must exchange it at once. Set both the OAuth login URI and the redirect
URI on the app in Developer Central.
Fallback: paste a refresh token. Validate it: a
refresh token starts with Atzr; amzn1.oa2-cs.…
is a client secret and amzn1.application-oa2-client.… a
client id, both commonly pasted by mistake.
Labels and transport: shipping labels come through
the API only for Direct Fulfillment vendors. Retail vendors such as Seba
book transport in Carrier Central and send the ASN in Vendor Central;
seba-erp does neither through the API today.
Price ceiling: see rule V2;
lib/vendor-pricing.ts records the real PO where Amazon
accepted a small rise.
Not in scope: supplier invoices and credit notes
stay in Vendor Central; Vendor remittances belong to the future
Accounting module.
Amazon Ads (ads)
Separate registration: a third Amazon application,
separate from SP-API, with its own LWA scope
(advertising::campaign_management), hostnames and refresh
token. A seller token cannot call the Ads API, and an Ads token cannot
call SP-API. (lib/amazon/ads.ts)
Profiles: every call is scoped to a
profile, one advertising account in one marketplace.
Reports are asynchronous; a first report can take
20 minutes. Run the request → poll → collect sequence as a
Workflow.
History: Amazon returns change history for 90 days
only.
Daily rhythm: structure sync, daily spend, nightly
engine proposals. Stagger tenants so they don't all hit the API at the
same minute (seba-erp staggers Ads jobs).
eBay (ebay)
Auth: our own eBay application with OAuth consent.
The seller registers nothing with eBay. (lib/ebay.ts)
Token lifetime: refresh tokens expire after about
18 months (verify); show reconnect warnings before then.
APIs: Sell APIs (Fulfillment for orders; Inventory,
or Trading for legacy listings, for stock). Check which listing model
Seba's listings use; older listings may not be visible through the
Inventory API.
Buyer data: full name, email, phone and address are
available, so retention rules apply.
Shopify (shopify)
Distribution and billing (verify with Shopify before
building):
The rule: an app listed in the Shopify App Store
must charge through Shopify's own billing (App Pricing / Billing API). A
merchant who installs from the App Store must not be able to reach a
Stripe checkout. Shopify keeps 0% of an app's first $1M of lifetime
revenue and 15% above that (since June 2025).
Start: use custom distribution (an
install link per merchant from the Partner Dashboard). There is no App
Store review, and Vervoro bills the tenant through Stripe as usual. This
is how Seba and other sales-led tenants connect.
A custom-distribution app installs on one store (or one Shopify Plus
organisation), so each merchant gets its own app with its own
credentials. Store those credentials encrypted per connection, like
tokens. This is the one exception to "app credentials are Worker
secrets".
Fine for a handful of sales-led tenants; it does not scale to
self-serve.
Later, for App Store growth: list the app. Tenants
who arrive through the App Store are billed through Shopify App Pricing
and their entitlements come from Shopify's billing events, alongside
Stripe. Keep the two billing paths fully separate.
Connections: OAuth per store. Several stores per
tenant are allowed.
API choice (verify the current rules): seba-erp
uses the REST Admin API. Shopify treats REST as legacy and requires new
public apps to use the GraphQL Admin API, so build
Vervoro's Shopify connector on GraphQL.
Rate limits: GraphQL uses a calculated query cost.
On a throttle, honour the provider's wait time; don't mark the SKU as
failed. seba-erp: lib/shopify.ts.
Webhooks: orders create/update, plus the mandatory
privacy webhooks (customer data request, customer redact, shop
redact).
TikTok Shop
(tiktok)
Request signing: HMAC-SHA256 over path + sorted
query parameters (excluding sign and
access_token) + body, wrapped in the app secret.
(lib/tiktok.ts)
APIs: order import and stock sync.
Partner app approval is required for other
sellers.
Each is a new connector module (see Adding a new sales
channel). Check each one's partner programme, approval rules and API
fees before committing to a date. US channels also bring USD and US
sales tax, which the data model already supports (money with currency,
tax per order line).
Stripe
See Commercial model.
Webhooks are verified, stored by event id and processed
idempotently.
Goal: Seba UK runs its daily operations on
Vervoro.
How: build P0, estimated at about 36 engineer-days
(Work packages). Then move Seba UK
with the standard customer
migration (D-058): import once, run the channels in shadow mode for
a few days, then switch over. The expected cutover is January
2027, after the go-live freeze; progress checkpoints turn the
estimate into a measured forecast as the build runs (D-065).
Priorities decide what moves if time runs short, never the domain rules.
Fixed points
No go-lives from 15 November to 31 December,
including Seba's own tenants (Operations). Connections already
in shadow may keep running.
Seba EU follows Seba UK, through the same migration
steps.
The first external customer comes after the legal
checklist, the marketplace approvals and P2.
Priorities
P0: needed to move Seba UK. Anything that writes
stock or that Seba needs to fulfil orders moves together, because
seba-erp and Vervoro must never both write stock.
Platform:
Identity and access: tenants and auth with
multi-factor sign-in, RBAC and warehouse scope, manual
entitlements, module registry, audit log.
Background work: jobs and the dispatcher cron,
outbox (with the suppressed status), rate limiter, token
encryption, the cross-tenant role.
Safety nets: the CI RLS check, staging in dry-run,
connection modes (shadow and active), no email or
documents from a shadow tenant, the isolation suite, error tracking and
backups with a rehearsed restore.
Stock and orders: products, inventory (warehouses,
locations, ledger, allocations), orders including manual and B2B,
minimal customers (needed by B2B orders and invoices),
dispatch documents, invoicing, basic warehouse tasks, returns
quarantine.
Phone use: every P0 screen works on a phone, the
warehouse and order screens designed phone-first; camera barcode
scanning (D-063, Architecture: UI).
Channels: SKU mapping with multiplier and
fulfilment type, unmatched SKUs, jobs view, the unmanaged-listings check
(rule C6).
Connectors: Amazon Seller (FBM orders by API, FBA
orders by report), eBay, Shopify and TikTok Shop.
Vendor Central: PO import, review and confirmation,
price ceiling, reservation, barcode matching, dispatch. ASN and labels
stay in Vendor Central and Carrier Central, as they do with seba-erp
today.
FBA: the inbound wizard on Amazon's step-by-step
Inbound API, as seba-erp does today, plus recording shipments created in
Seller Central (rule S10) (D-064).
Purchasing: suppliers, POs, receiving into batches,
landed cost.
Migration: the template importers and the cutover
steps in Customer
migration.
Amazon data protection (from the first shadow
connection):
the buyer-data anonymiser job;
multi-factor sign-in;
security logs to R2 for 12 months;
the incident procedure with Amazon's 24-hour notice (Operations).
Monthly cost-of-goods export for seba-erp's
accounting (D-047), before the first month-end after cutover.
P1: in the weeks after cutover. These don't write
stock, so seba-erp keeps running them until they are ready.
MCP connector (D-053): OAuth, read tools,
listing-change and PO proposals with approval screens; then the
remaining propose tools. seba-erp's assistant stays available read-only
until then.
Vendor ASN and carton/pallet labels. New compared
with seba-erp; needed before the first external Vendor customer who
wants them.
Catalogue-from-listings importer (D-035), for
sellers with no system such as Seba EU. Opening stock uses the P0
template importer.
Reporting and admin:
Dashboard and basic reports; customers' buyer matching.
Amazon settlement import (Finances API), which
feeds Core's basic profit per SKU and Growth's payout profitability. Ad
spend joins the profit report only when the Ads module exists.
Amazon Ads engine. Seba keeps running it in
seba-erp; it never touches stock.
Accounting.
Work packages
P0 split into packages with a first estimate in engineer-days. Two
engineers work with AI coding tools, roughly one lane each. The
estimates are a starting point; the progress checkpoints replace
them with measured speed.
Id
Package
Lane
Estimate (engineer-days)
A1
Repo, CI, environments, Neon + Hyperdrive, Drizzle, roles,
withTenant, RLS and the CI check
A
3
A2
Better Auth with MFA, RBAC and warehouse scope, entitlements, module
manifest, audit log
A
3
A3
Products (kits, packs, barcodes) and the template importers
Not estimated yet: the monthly export for seba-erp's accounting
(D-047), until its contents are agreed.
Each connector can be connected to Seba's account in shadow mode as
soon as it is built, once the data processing agreement is signed and
the data-protection items are live.
Progress checkpoints
Instead of a fixed date, measure the build as it runs and forecast
from real speed.
When: every two to three working days, with the
demo to the product owner.
Who: an AI agent (e.g. Claude Code, run by hand or
on a schedule) with read access to the application repository and this
document.
What it reads: the work packages table; merged pull
requests, open issues and branches since the last checkpoint; which
domain-rule tests exist by rule id; the isolation suite's status; the
previous checkpoint.
What it writes: a short report in the application
repository, progress/YYYY-MM-DD.md:
each package: done, in progress (with a rough share done) or not
started; done means merged, its rule tests present and the isolation
suite green;
speed: estimated engineer-days completed ÷
engineer-days actually spent so far;
forecast: remaining P0 estimate ÷ speed, as working
days and a date, then the same for P1 and P2;
packages that grew, new work found, and risks to the cutover.
What the owner does with it: move scope between P0
and P1, or move the cutover. Decisions go into Decisions.
Add new decisions at the bottom with the next number. To change one,
add a new decision, mark the old one superseded and cut it to one line.
Only decisions marked Live (or Proposed, once confirmed)
apply. AI agents must ignore superseded rows.
Decision log
#
Date
Decision
By
Status
D-001
2026-10-06
Build Vervoro from scratch on Cloudflare serverless
(Workers, Queues, Workflows, Durable Objects, R2, Hyperdrive). No
VMs
Product owner
Live
D-002
2026-10-06
TypeScript end to end. Go, Vue/PrimeVue, Keycloak,
RabbitMQ, Docker Compose and the Hetzner/UpCloud hosting package are
dropped
Product owner
Live
D-003
2026-10-06
Vervoro is seba-erp as a commercial product: the
same functional scope, rebuilt with multi-tenancy, RBAC and sellable
modules. seba-erp is the read-only reference for behaviour, not for
architecture
Product owner
Live
D-004
2026-10-06
The handover to the build team is docs/ in this repo,
plus read-only access to seba-erp. The engineers build from scratch; no
seba-erp code is ported
Product owner
Live (seba-erp access as in D-031)
D-005
2026-10-06
First goal: Seba Trade runs daily operations on
Vervoro (tenant #1)
Product owner
Live; timing per D-059
D-006
2026-10-06
Core plan, priced by orders/month tiers, includes
products, inventory, orders, warehouse tasks, customers, invoicing,
basic reports, channels and one connector account.
Users are unlimited
Connectors (Amazon Seller, eBay, Shopify, TikTok
Shop): the first account is in Core; each further connector or account
is paid
Product owner
Superseded by D-037
D-009
2026-10-06
Sales invoicing / B2B is in Core. Bank
reconciliation and bookkeeping belong to Accounting
Product owner
Live
D-010
2026-10-06
Reporting split: basic in Core; payout
profitability, pricing/Buy Box and rank tracking in the Advanced
reporting add-on
Product owner
Superseded by D-037 / D-039
D-011
2026-10-06
Accounting is left out for now. It stays in
seba-erp for Seba and becomes a Vervoro module later
Product owner
Live
D-012
2026-10-06
Self-serve sign-up with Stripe subscriptions and
per-module add-ons. Built after Seba's cutover (P2)
Product owner
Live
D-013
2026-10-06
One shared Postgres database with tenant_id and
forced RLS (not a database per tenant)
Carried from the earlier VM-era design (its ADR 0002)
Live
D-014
2026-10-06
Amazon Ads engine is out of the first version.
Lowest priority; built later. Seba keeps running it in seba-erp
Product owner
Live
D-015
2026-10-06
MCP connector is P0 (read tools, listing-change and
purchase-order proposals). In-app AI features that spend Vervoro's
tokens are P1/P2
Product owner
Superseded by D-053
D-016
2026-10-06
Purchase orders are in the first version
(Purchasing is P0 for Seba)
Product owner
Live
D-017
2026-10-06
Engineers without seba-erp repository access use
reference/seba-erp/ (a curated copy of 50 files) plus, if
possible, a read-only login to the running app
Product owner
Live
D-018
2026-10-06
Postgres stays (no D1, Durable Object SQLite,
MySQL, CockroachDB or Aurora DSQL as the system of record).
Host: Neon London, Launch plan now, Scale before the
first external tenant. Runner-up: PlanetScale Postgres
Hyperdrive query caching off; explicit
tenant_id filter in every query; transaction timeouts and
lock ordering; previews connect directly to masked Neon branches
Stack debate
Live
D-020
2026-10-06
US expansion = a second regional cell (database +
Hyperdrive config + R2/Durable Object jurisdiction), added only when a
contract needs it. The code is region-ready from day 1
Stack debate
Live
D-021
2026-10-06
No Sentry at launch. Workers Logs + Workers Issues
+ browser-error relay + 5-minute health cron. Sentry Team is the
fallback
Stack debate
Live
D-022
2026-10-06
Apply to the Neon (Databricks) and Cloudflare startup
programs. Credits don't change any technical choice
Stack debate
Superseded by D-030
D-023
2026-10-06
Vervoro is self-funded: apply for the self-funded
tiers (Neon up to $1k; Cloudflare for Startups' bootstrapped tier)
Product owner
Superseded by D-030
D-024
2026-10-06
Seba's volume: Seba UK ~1,000 orders/day including
FBA (~30k/month), ~150/day non-FBA. Seba EU has about the same
volume
Product owner
Live
D-025
2026-10-06
Hyperdrive query caching stays off. Revisit only
for a separate binding that serves non-tenant reference data
Product owner
Live
D-026
2026-10-06
Seba EU is a separate tenant: its own company and
its own warehouse(s). A tenant can have several warehouses from the
first version (rule O9)
Product owner
Live
D-027
2026-10-06
Seba EU moves to Vervoro about one month after Seba
UK. It does not run on seba-erp today, so its data import is a
separate piece of work
Product owner
Live; timing per D-059
D-028
2026-10-06
Amazon SP-API: register Vervoro as a
public developer with an unlisted app
(up to 25 sellers by OAuth, vendors unlimited); list on the Appstore
before seller 25. A private app can't serve other companies. Seba UK may
use Seba's private app as a bridge until approval
Product owner question; recommendation per Amazon's limits
Live
D-029
2026-10-06
Accounting (later) = a shared core plus regional
packs (UK, EU and later US), integrating with Xero, DATEV or
QuickBooks rather than replacing them. The first version stores the tax
and currency data these packs will need
Product owner direction; structure recommended
Live
D-030
2026-10-06
Startup credits are left out of planning and cost
figures. Supersedes D-022 and D-023
Product owner
Live
D-031
2026-10-06
Engineers get a read-only login to the seba-erp web
app (not the code or database). Behaviour reference: the app
plus reference/seba-erp/
Product owner
Live
D-032
2026-10-06
TikTok Shop is P0: Seba sells actively on it
Product owner
Live
D-033
2026-10-06
New sales channels must be addable as connector modules
without core changes (US: Walmart, Target and others). Channel
keys are strings, not a hard-coded enum; every connector passes a shared
conformance test kit
Product owner
Live
D-034
2026-10-06
Seba UK and Seba EU have one warehouse each; the
system supports several warehouses per tenant from the first
version
Product owner
Live
D-035
2026-10-06
Seba EU uses no system today, so it onboards
through Vervoro's own onboarding imports (catalogue from channel
listings, opening stock CSV). These imports must be ready before Seba
EU's move
Product owner
Live
D-036
2026-10-06
Seba UK's data reaches Vervoro through an exporter script in
seba-erp
Product owner
Superseded by D-060
D-037
2026-10-07
Pricing: three feature plans from the open pricing
debate: Core £99 / Growth £279 / Pro £549 (€119/€329/€649, 129/359/$699), each with an own-warehouse
order ceiling (1,500 / 5,000 / 15,000, 3-month average, FBA never
counted, no overage). Users, channels, warehouses, batches and AI
connector unlimited in every plan. Vendor Central £199 add-on. Founder
rate for the first 25. Supersedes D-006, D-007, D-008 and
D-010; D-009 (invoicing in Core) stands
Product owner
Live
D-038
2026-10-07
Pro adds no extra features beyond Growth: higher
order ceiling and priority support only. API access, multi-company
accounts and approval workflows proposed by the debate are out of
scope
Product owner
Live
D-039
2026-10-07
Detailed reporting is part of Growth, not a separate paid
add-on. Core keeps a basic profit-per-SKU report
Product owner (confirm)
Live (confirmed, D-055)
D-040
2026-10-07
Everything is built as a module (add-on); plans are
versioned data that bundle modules and limits. Code checks
module entitlements and limit quantities, never plan names, so modules
can move between plans or be sold separately without code changes
Product owner
Live
D-041
2026-10-07
Permissions and roles as in 10-permissions.md: six built-in roles, a
module-declared permission catalogue, per-user warehouse scope, and a
data model ready for custom roles later
Product owner
Live
D-042
2026-10-07
Shopify: start with custom distribution billed
through Stripe; list in the App Store later, with App Store tenants
billed through Shopify (part of the accepted pricing model, D-037)
Product owner
Live
D-043
2026-10-07
Warehouse role keeps access to buyer addresses
(needed to pack and label)
Product owner
Live
D-044
2026-10-07
Operations and launch as in 11-operations-and-launch.md:
support levels by plan, status page, incident severities, no go-lives 15
Nov–31 Dec, data retention, legal checklist before the first external
tenant
Recommended; owner to confirm the legal items with a solicitor
Live
D-045
2026-10-07
Contacts: product owner Muhammet Surucu (owner of
Seba Trade); engineers Yusuf Figanioglu and Tolga Sahin
Product owner
Live
D-046
2026-10-07
A new company is being opened for Vervoro. It owns
the marketplace developer accounts, the Stripe account and the code, and
signs the engineer NDA and IP assignment and the customer terms
Product owner
Live; interim arrangements per D-054
D-047
2026-10-07
Seba's accounting stays in seba-erp, fed by a monthly export
from Vervoro: dispatched units with the landed cost of their
batches, until Vervoro's Accounting module exists
Product owner
Live
D-048
2026-10-07
Ads campaigns Vervoro creates are prefixed
VRV- (configurable per tenant). Seba's existing
ERP- campaigns are recognised as legacy and kept
Product owner
Live
D-049
2026-10-07
Data model as in 12-data-model.md: batches never split, stock
held in stock_balance rows by location and status
(quarantine is a status), allocations for every reservation, one
dispatch per order
Docs review 2026-10-07
Live
D-050
2026-10-07
Amazon data protection applies from cutover (P0):
buyer data anonymised 30 days after delivery, MFA for tenants with an
Amazon connection, 12-month security logs, Amazon notified within 24
hours of an incident
Docs review 2026-10-07
Live; applies from the first shadow connection
D-051
2026-10-07
Cutover safety: staging never sends to real
marketplaces; unmanaged listings cleared before stock push (rule C6).
Other cutover steps are in D-058
Docs review 2026-10-07
Live
D-052
2026-10-07
Option B: FBA shipments built in Seller Central and Vendor
ASN/labels in Vendor Central for Seba UK's move
Product owner
Superseded by D-064
D-053
2026-10-07
The MCP connector comes after Seba UK's cutover
(P1). seba-erp's assistant stays available read-only until
then. Supersedes D-015's P0
Product owner
Live
D-054
2026-10-07
The product owner arranges the interim legal entity and the
Seba Trade ↔︎ Vervoro data processing agreement, plus the
engineers' NDAs
Product owner
Live: NDAs before engineers receive any of Seba's data, the DPA
before the first shadow connection
D-055
2026-10-07
D-039 and the proposed commercial defaults in 03 are
confirmed
Product owner
Live
D-056
2026-10-07
Parallel run with nightly re-sync from seba-erp
Product owner
Superseded by D-058
D-057
2026-10-07
Parallel-run mechanics. Still live from it: shadow
sends stored as suppressed and never sent; no email or
documents from a tenant with shadow connections; Amazon data protection,
NDAs and the data processing agreement in place before the first shadow
connection
Focused review 2026-10-07
Superseded by D-058, except the points listed
D-058
2026-10-07
Simple, standard migration for every customer, Seba
UK first (Customer migration):
import master data, connect channels in shadow and check
orders for a few days, then at cutover stop the old system's sync,
import stock once plus the open items, and switch connections to
active. The old system is kept at least two weeks as the
fallback. Supersedes D-056 and D-057
Product owner
Live
D-059
2026-10-07
Timeline is managed by the product owner. The docs
set no fixed build length
Product owner
Live; expected cutover per D-065
D-060
2026-10-07
Vervoro has importers only, against its own versioned CSV
templates. Every source fills the templates: by hand, or later
by a direct API importer. How to migrate from custom in-house software
is designed after the system is built. The engineers get no seba-erp
database access. Supersedes D-036
Product owner
Live
D-061
2026-10-07
Direct API importers (Linnworks first, then the
most common sources) are a planned investment after the first version.
They fill the same templates
Product owner
Live (later)
D-062
2026-10-07
The product is named Vervoro. Short prefix
VRV
Product owner
Live
D-063
2026-10-08
Mobile-first where practical, and every screen works on a
phone. Warehouse, order, Vendor approval and product screens
are designed for the phone first; big tables, reports and settings must
work on a phone but needn't be perfect there. Camera barcode
scanning alongside hardware scanners
Product owner
Live
D-064
2026-10-08
The FBA inbound wizard is P0, as seba-erp has it
today. Vendor ASN and labels stay out of P0: seba-erp doesn't do them
(Seba uses Vendor Central and Carrier Central), so they are a P1
addition. Supersedes D-052
Product owner
Live
D-065
2026-10-08
P0 is estimated at about 36 engineer-days; expected cutover
January 2027. Progress checkpoints every two to three working
days measure speed and forecast the finish (Delivery plan)
Product owner
Live
What
the earlier design said, and what happened to it
Today's list prices (checked 2026-10-06), excluding VAT. Cloudflare
and Neon bill in USD; Stripe in GBP. These are infrastructure costs
only, not salaries or tools for the engineers.
Launch plan now: $0.106 per compute-hour,
$0.35/GB-month storage, 7-day restore. Scale plan
before the first external tenant: $0.222 per compute-hour, 30-day
restore. No monthly minimum
Cloudflare R2
Files, labels, PDFs, nightly database backup
First 10 GB free, then $0.015/GB-month
Domain
vervoro domain; email forwarding through Cloudflare Email Routing
(free)
About $1/month
GitHub
Code, CI, nightly backup job
$0 on the Free plan
Everything else is $0 today:
Item
Cost
Amazon SP-API (Seller, Vendor)
$0
eBay, Shopify, TikTok APIs
$0
Shopify app
$0 (custom distribution; we bill through Stripe)
Sentry
$0 (not used)
AI through the MCP connector
$0 (the customer's own Claude or ChatGPT pays the tokens)
Amazon Ads engine, Accounting
$0 (not built yet)
Monthly cost by stage
Stage
Load
Cloudflare
Neon
R2 + domain
Total / month
Build
Development only
$5
~$10
$1
~$16
Seba UK live
~1,000 orders/day incl. FBA, ~150 fulfilled
$5
~30(Launch : prod0.25computealwayson20–25,
staging and previews ~$8)
$1
~$36
Seba UK + Seba EU
~2,000 orders/day, ~300 fulfilled
$5
~$40–55
$1
~$46–61
First external tenant (switch to Scale)
+ 1 customer
$5
~$100 (Scale, prod ~0.5 compute + staging and previews)
$1
~$106
10 tenants
~$6
~$180 (Scale, ~1 compute average)
$2
~$190 (≈ $19 per tenant)
50 tenants
~$30
~$370 (Scale, ~2 compute average, ~80 GB)
$3
~$400 (≈ $8 per tenant)
The database is almost the whole bill. Its size per tenant is an
estimate; the real figure depends on query efficiency and how often jobs
run. Workers costs stay at the $5 base until the tenant count is
large.
Stripe fees (only
when a customer pays)
Seba UK and Seba EU are internal tenants and pay nothing through
Stripe. For a paying UK customer on a £100/month plan, Stripe charges on
the amount collected, £120 including 20% VAT:
Payment method
Processing
Billing (0.7%)
Tax (0.5%)
Total
Share of the £100 you keep before VAT
UK standard card (1.5% + 20p)
£2.00
£0.84
£0.60
£3.44
3.4%
UK premium or business card (2.8% + 20p)
£3.56
£0.84
£0.60
£5.00
5.0%
Bacs Direct Debit (1%, minimum 20p, capped at £4)
£1.20
£0.84
£0.60
£2.64
2.6%
Many of our customers are businesses paying by business card, so
plan for about 4–5%.
Offer Bacs Direct Debit as the default for UK
customers, especially on annual plans, where its £4 cap makes it far
cheaper than cards.
EEA cards cost 2.5% + 20p, plus 2% if Stripe converts the currency;
take EUR payments into a EUR balance to avoid the conversion fee.
Status page service (or a self-hosted page on Workers).
Help centre and support inbox tool.
External penetration test (one-off).
Solicitor: customer terms, data processing agreements, privacy
policy, NDAs (one-off).
ICO data protection fee (yearly) and company formation for Vervoro
(one-off).
Compared with the
earlier VM plan
The VM design (Hetzner + UpCloud, Keycloak, RabbitMQ) was budgeted at
about €135/month for one customer, plus server
administration. The serverless design costs about
$36/month for Seba UK, with no servers to run.
A user can act only when all three are true: the
tenant has the module (entitled and enabled; see Commercial
model), the user's role grants the permission, and, for warehouse
work, the record is in a warehouse the user may access. This document
lists every first-version permission, which built-in role gets it, and
how warehouse scoping and future custom roles work.
How it works
A permission is a string,
module.resource.action (e.g. orders.dispatch).
Each module declares its permissions in its manifest (Architecture: Modules). The tables
below are the first-version catalogue and the seed for the built-in
roles.
A role is a named set of permissions. A
membership (Better Auth's member row) links a user
to a tenant with one or more roles and a warehouse scope. In the first
version the UI assigns one role per user; the data model allows several,
and a user's permissions are the union.
Effective permissions = the role's permissions,
minus anything belonging to a module the tenant doesn't have. A role can
never unlock a module the tenant hasn't bought.
Checked on the server on every request with
requirePermission("…"), evaluated fresh for each request
(no long-lived permission cache). A role change or removal takes effect
on the user's next request, including requests from their MCP
connections.
The UI hides what a user can't do, but it is never
the security boundary.
Everything, including billing, deleting the tenant and transferring
ownership
Admin
Office lead
Everything except billing, deletion and ownership
Manager
Operations lead
All operational work, approvals and module settings; no user
management or billing
Warehouse
Pickers, packers, goods-in
Stock, receiving, picking, packing and dispatch in their warehouses.
No cost prices, margins or exports
Accountant
Bookkeeper, external accountant, reviewer
Read and export everything, costs included; change nothing
Staff
Anyone else
Clock in/out, own leave, own assigned tasks
Built-in roles are defined in code, versioned, and cannot be edited
by a tenant. Every role includes attendance.self and
tasks.self, so everyone can clock in and work their own
tasks.
Who can change whose
access
Only an Owner grants or removes the Owner role
(tenant.owners.manage). Admins manage everyone else.
Nobody can change their own role or warehouse scope.
The last remaining Owner can't be removed or demoted; transfer
ownership first.
Every role or scope change is audit-logged and shows in the security
log.
Permission catalogue
Permission prefixes are resource names, not always the module key.
Each module declares these prefixes in its manifest:
✓ = granted to that built-in role. O Owner ·
A Admin · M Manager ·
W Warehouse · Acc Accountant ·
S Staff.
Account and platform
Permission
Allows
O
A
M
W
Acc
S
tenant.billing.manage
Plans, add-ons, payment method, invoices from Vervoro
✓
tenant.delete ·
tenant.ownership.transfer
Delete the tenant; make someone else Owner
✓
tenant.owners.manage
Make someone Owner or remove an Owner
✓
tenant.users.manage
Invite and remove users, assign any role except
Owner, set warehouse scope
✓
✓
tenant.settings.manage
Company profile, documents, email settings, next invoice/PO
numbers
✓
✓
tenant.modules.manage
Switch entitled modules on or off; module settings
✓
✓
✓
audit.read
Read the audit log
✓
✓
✓
✓
jobs.read · jobs.retry
Jobs view; retry a failed job
✓
✓
✓
read
dashboard.read
Dashboard
✓
✓
✓
✓
✓
Sensitive data
(cross-module)
Permission
Allows
O
A
M
W
Acc
S
finance.costs.read
Cost prices, landed cost, margins, profit
✓
✓
✓
✓
orders.buyer_pii.read
Buyer names, emails, phones and full addresses on screen and
documents
✓
✓
✓
✓
✓
data.export
Any CSV/JSON export (each export also needs that module's read
permission)
✓
✓
✓
✓
Without finance.costs.read, cost and margin fields are
removed by the API, not just hidden. Without
orders.buyer_pii.read, buyer fields come back masked; a
Warehouse user needs it to pack and label, which is why it is granted to
that role.
Products, inventory
and warehouses
Permission
Allows
O
A
M
W
Acc
S
products.read
Products, bundles, packs, barcodes
✓
✓
✓
✓
✓
products.write
Create and edit products, bundles, box configs
✓
✓
✓
products.import
CSV and catalogue imports
✓
✓
✓
inventory.read
Stock, batches, movements, traceability
✓
✓
✓
✓
✓
inventory.receive
Goods in without a PO; opening stock. A reason is required, every
receipt is audited and listed in the reconciliation report
✓
✓
✓
✓
inventory.transfer
Move stock between locations
✓
✓
✓
✓
inventory.returns.inspect
Inspect quarantined returns
✓
✓
✓
✓
inventory.repack
Repack jobs (components ↔︎ packs)
✓
✓
✓
✓
inventory.adjust
Adjustments and write-offs (reason required)
✓
✓
✓
warehouses.manage
Create and edit warehouses and locations
✓
✓
✓
Orders, customers and
invoicing
Permission
Allows
O
A
M
W
Acc
S
orders.read
Order list and details
✓
✓
✓
✓
✓
orders.create
Manual and B2B orders
✓
✓
✓
orders.edit
Pre-dispatch edits, change an order's warehouse
✓
✓
✓
orders.cancel
Cancel an order or request cancellation
✓
✓
✓
orders.pick
Pick lists, picking slips, packing lists
✓
✓
✓
✓
orders.dispatch
Confirm dispatch, record tracking
✓
✓
✓
✓
customers.read · customers.write
Customers; create and edit
✓
✓
✓
read
invoices.read
Sales invoices and payments
✓
✓
✓
✓
invoices.write ·
invoices.payments.record
Create and send invoices; record payments
✓
✓
✓
Channels and reports
Permission
Allows
O
A
M
W
Acc
S
channels.read
Connections, listings, mappings, sync status
✓
✓
✓
✓
channels.connect
Connect, reconnect, disconnect channel accounts
✓
✓
channels.mapping.write
SKU mapping, unmatched SKUs
✓
✓
✓
channels.listings.write
Change listing content on a channel (e.g. apply an approved AI
listing proposal)
✓
✓
✓
channels.stock_push.manage
Switch a connection between shadow and
active (stock push on or off); set buffers
✓
✓
✓
reports.read
Basic reports
✓
✓
✓
✓
reports.advanced.read
Detailed reporting (Growth)
✓
✓
✓
✓
Growth modules and
add-ons
Permission
Allows
O
A
M
W
Acc
S
fba.read
FBA shipments
✓
✓
✓
✓
✓
fba.plan
Create and change inbound plans
✓
✓
✓
fba.pack
Box contents, labels, confirm handover
✓
✓
✓
✓
purchasing.read
Suppliers and purchase orders (prices and costs on them only with
finance.costs.read, so Warehouse sees lines and quantities,
not prices)
✓
✓
✓
✓
✓
purchasing.write
Suppliers, draft POs, supplier codes
✓
✓
✓
purchasing.approve
Approve and send POs to suppliers
✓
✓
✓
purchasing.receive
Receive against a PO into batches
✓
✓
✓
✓
vendor.read
Vendor Central POs
✓
✓
✓
✓
✓
vendor.confirm
Approve and confirm Vendor POs to Amazon
✓
✓
✓
vendor.ship
Carton/pallet labels, ASN (P1)
✓
✓
✓
✓
tasks.self
See and update own assigned tasks
✓
✓
✓
✓
✓
✓
tasks.manage
Create, assign and close any task
✓
✓
✓
attendance.self
Clock in/out, own records, request leave
✓
✓
✓
✓
✓
✓
attendance.team.read
Team attendance and leave
✓
✓
✓
✓
attendance.manage
Correct records, approve leave, settings
✓
✓
✓
AI & MCP
Permission
Allows
O
A
M
W
Acc
S
ai.connect
Connect your own Claude/ChatGPT through MCP (it acts with your
permissions)
✓
✓
✓
✓
✓
ai.proposals.review
Approve or reject AI proposals (each apply also needs the target's
write permission, e.g. purchasing.write for a PO,
channels.listings.write for a listing change)
✓
✓
✓
ai.connections.manage
See and revoke anyone's MCP connections
✓
✓
Warehouse scope
Each membership has a warehouse scope: all
warehouses (the default) or a list of warehouses.
Scoped: stock, batches, movements, receiving,
transfers, returns, repacks, and orders, FBA shipments, Vendor POs, PO
receipts and tasks assigned to a warehouse in scope.
Out-of-scope records are invisible, as if they don't exist (404, not
403).
Not scoped: the shared catalogue, channel settings
and anything tenant-wide. Reports and dashboards for a scoped user
include only their warehouses.
Enforced in the inventory and order services
through one helper (scopeWarehouses(ctx)) that every
warehouse-bound query uses, with a scope test suite: a
user scoped to warehouse A calls every warehouse-bound route with
warehouse B's ids and must get nothing.
Owner and Admin are unscoped by definition, so
making someone Admin widens their access; only Owners and Admins can do
that, and only an Owner can make someone Owner. For every other role,
changing the role never changes the warehouse scope.
Seba UK and Seba EU have one warehouse each today, so scope is "all"
for everyone; the feature exists for tenants with several sites.
One membership table: Better Auth's organization
plugin already keeps member (which user belongs to which
tenant). Vervoro uses that table as the membership, adds
warehouse_scope to it, and keeps roles in its own
role, role_permission and
membership_role tables. Better Auth's own
member.role field is not used for
authorization; Vervoro's tables are the only source of truth.
Built-in roles have tenant_id NULL
and are seeded from code. Changing a built-in role's permissions is a
code change with a new version, recorded in the decision log.
Custom roles (later): a tenant creates a role
row with its own tenant_id and picks permissions from the
catalogue; built-in roles can be cloned as a starting point. No schema
change is needed.
Tests: CI checks that every API route calls
requirePermission, that every permission key used in code
exists in a manifest, and that every key in a manifest appears in this
document.
Adding a module
A new module ships with its permissions in its manifest, a row for
each in this document, and a decision on which built-in roles get them.
Until that decision is made, only Owner and Admin get the new
permissions.
What has to exist, besides the product, before Vervoro takes money
from an outside customer: support, incident handling, data retention,
legal paperwork and the numbers to watch. Some of it is needed
before Seba UK's first shadow connection, because
Vervoro then holds Seba's data from Amazon and other channels: the Amazon
data-protection rules, the incident procedure, backups, and the
legal items marked before the first shadow connection. The rest
must be ready before the first external tenant.
Support
Core
Growth
Pro
Help centre (self-serve articles)
✓
✓
✓
Email support
Reply within 2 working days
Reply within 2 working days
Priority: reply within 1 working day
Onboarding call
— (paid packages only)
One call
One call
One support inbox (e.g. support@ on the Vervoro
domain), with every conversation tagged by tenant.
Log support time per tenant from the first external
tenant (P1). The pricing review at founding customer 10 depends on it
(Commercial model).
Support access to a tenant's data uses the platform
admin's time-limited, audited access; never a shared login.
Help centre first: write an article every time the
same question comes in twice. Core customers are self-serve; the support
load decides whether Core stays profitable at its price.
Working hours: UK business days. Outside them, only incidents
(below) are handled.
Status page and incidents
Public status page (on its own domain or a
hosted status service) showing app, API, and each channel's sync
(Amazon, eBay, Shopify, TikTok). The health cron's checks (Architecture: Observability)
feed it.
Severity:
Level
Example
Response
Critical
Stock push or order import stopped for any tenant; data exposed
across tenants; app down
Page an engineer immediately, any time; status page within 30
minutes; update every hour
Major
One channel or one module failing for several tenants
Next working hour; status page update
Minor
A single tenant's problem with a workaround
Normal support queue
Overselling risk first: if stock push is wrong
or stale, pause stock push for affected accounts (or set channel
quantities to a safe value) before investigating.
After every critical incident: a short written
review within 5 working days (what happened, impact, fix, prevention)
shared with affected customers.
Service credits: capped at one month's fee and
the only remedy, as in the customer terms.
Peak season: no go-lives between 15 November and
31 December, for customers and for Seba's own tenants alike; an engineer
on call each day of that period, weekends included. Connections already
in shadow mode may keep running.
Security incidents involving personal data
(cross-tenant exposure, leaked tokens or exports, unauthorised access)
are always Critical:
Amazon within 24 hours (security@amazon.com) whenever
Amazon data could be involved, as Amazon's Data Protection Policy
requires;
the ICO within 72 hours where the law
requires;
the affected tenants (and Seba, as controller of its buyers' data)
without delay.
Amazon
data protection (from the first shadow connection)
Amazon's Data Protection Policy applies to every tenant with an
Amazon connection, Seba included, from the first day Vervoro holds
Amazon data, which is the first shadow connection:
Buyer personal data is kept no longer than
30 days after delivery, then anonymised by the nightly
job (P0). This covers orders, stored raw marketplace payloads, webhook
and inbox rows, and links from orders to customer records.
Multi-factor sign-in for every user of such tenants
(P0).
Encryption at rest for personal data and tokens;
never stored on personal devices. A customer's unmasked import files are
handled only on a company-managed, encrypted machine and deleted after
the import.
Security logs kept at least 12 months, without
buyer data (Logpush to R2, P0).
Incident notice to Amazon within 24 hours
(above).
Least access: only the people who need it see buyer
data (orders.buyer_pii.read), and MCP never returns
it.
As little buyer data as possible in shadow mode: a
connection in shadow requests no Amazon Restricted Data Token, so Amazon
orders carry no buyer name or address, and screens show buyers masked.
The shadow check needs none of it. eBay, Shopify and TikTok send
addresses anyway, so the anonymiser runs from the first shadow day.
Data retention
Data
Kept
Then
Buyer personal data on orders, raw marketplace payloads,
webhook/inbox rows and order-to-customer links
30 days after delivery for Amazon orders (Amazon's
Data Protection Policy); the same limit is applied to every channel for
simplicity
Anonymised automatically: name, street, email and phone removed;
postcode district, city and country kept for reporting
Orders, stock movements, invoices, purchase records
While the tenant is subscribed. Keeping tax records (6 years in the
UK) is the customer's duty; they export before leaving
Point-in-time restore window (7 days, 30 on Scale); encrypted
nightly dumps 14 days
Deleted. Backups can hold buyer data a little past 30 days; this is
the only exception and must be confirmed as acceptable under Amazon's
policy before launch
Cancelled tenant
30 days read-only and export
Hard-deleted, files and backups included as they expire; the
deletion itself is audit-logged
A nightly job applies these rules and reports what it anonymised or
deleted.
Legal and compliance
checklist
The product owner owns these; most need a solicitor.
Before engineers receive any of Seba's data, masked or
not (the masked copy still holds real costs and supplier
prices):
Before the first shadow connection (about build day
4–5):
Before the first external tenant:
Website and sales assets
Public website with the pricing page (Commercial model),
terms, privacy policy and status page link.
Demo tenant with synthetic sample
data (generated products, suppliers, costs, orders, Amazon, FBA, Vendor,
batches with best-before dates), never Seba's data even masked, so
prospects can explore without connecting their own Amazon account.
Seba as the reference customer: a short case study
with real volumes, once Seba UK has run on Vervoro for a month.
Numbers to
watch (per tenant, from day one)
Own-warehouse orders per month against the plan ceiling (3-month
average).
Support hours per month.
Amazon API calls (GET) per month.
Stock-push delays and sync failures.
Infrastructure cost per tenant (database time, storage).
Review them at founding customer 10 against the triggers in the pricing
report.
The core tables and status flows, so every engineer and every AI
session builds the same schema. Column lists are the minimum; add
columns freely, but don't rename these concepts or change their keys
without a decision entry. Rules referenced (S1, C2 …) are in Domain rules.
Conventions
Every tenant table has id (UUID v7),
tenant_id, created_at,
updated_at, created_by, forced RLS, and
composite foreign keys that include tenant_id (Architecture: Tenancy). Editable
records also have version (optimistic concurrency).
Quantities are integers in base units. Money is
numeric(14,4) with a currency code on the same row.
Natural keys are unique per tenant (or per
connection), never globally.
Where it is and in what state. A partial transfer
moves quantity between two balance rows of the same batch
movement
type (incl. received, transferred,
dispatched, adjusted, returned,
written_off, repacked_in/out),
product, batch, from (location, status), to (location, status), quantity
> 0, reason, actor, source type and id
Immutable ledger. on_hand of every balance row equals
the sum of its movements (S1)
allocation
demand type (order_line, vendor_po_line,
fba_shipment_line), demand line, batch, location, quantity,
status: reserved, picked,
consumed, released
A balance row's reserved equals the sum of its
reserved + picked allocations (S2)
Quarantine is a status, not a location. A return is
received into a returns location with status
quarantine; inspection moves it to saleable
(normally into a storage location), damaged, or writes it
off. Sellable stock counts only saleable balance rows of
non-expired batches (S3).
Channels
Table
Key columns
Notes
connection
connector key (string, e.g. amazon-seller), native
account id (unique per tenant and connector), name, marketplaces,
default warehouse, mode: shadow or
active, status, encrypted tokens and key version,
app credentials (for apps owned per tenant: Seba's existing bridge apps
and custom-distribution apps)
One per connected account (C1). Only active connections
send anything
listing
connection, channel SKU, native reference (ASIN, item id, variant
id), fulfilment: merchant or amazon, active,
last observed quantity
listing, desired quantity, last pushed quantity, pushed_at,
confirmed_at
Push only to merchant listings (C5)
Orders, customers,
invoicing
Table
Key columns
Notes
customer
type: b2b, b2c; name, company, email,
phone, addresses, VAT number
Buyer links follow the retention rules
order
(tenant, connection, native order id) unique; NULL connection for
manual/B2B; fulfilment: merchant or amazon;
warehouse; currency; status; stock status; ordered_at; dispatched_at;
buyer fields (anonymised after 30 days)
Amazon-fulfilled orders never get a stock status (O6)
order_line
order, channel SKU, product (NULL while unmapped), channel quantity,
base quantity (= channel quantity × multiplier), unit
price, tax rate, tax amount, tax country
dispatch
order (one per order: no partial dispatch in V1), carrier, tracking,
dispatched_at, actor, document snapshot
Consumes the order's allocations once (O8)
return · return_line
order, received_at, lines with quantity and inspection outcome
Received into quarantine (S9)
invoice · invoice_line ·
payment
invoice number (unique per tenant, from
number_sequence), customer, status, totals and
currency
B2B and direct sales
Purchasing, Vendor, FBA
Table
Key columns
Notes
supplier · supplier_code
supplier; (supplier, code) unique → product, units per pack
PU2
purchase_order · po_line ·
po_receipt
PO number unique per tenant; status; currency
A receipt creates batches and received movements
vendor_po · vendor_po_line
(connection, PO number) unique; status; acknowledgement status and
time; ship window; warehouse; lines with requested and accepted
quantity, PO cost, confirmed cost
none → partially_reserved →
reserved as stock is allocated
ready
picking, cancel_requested,
dispatched_elsewhere
reserved
picking
dispatched, cancel_requested
picked
dispatched
delivered, returned
dispatched (allocations consumed)
cancel_requested
cancelled, back to the previous status if refused
Unchanged until confirmed (O5)
cancelled
—
Allocations released once
dispatched_elsewhere
—
Migration only: the old system shipped it. Allocations released, no
stock movement
Amazon-fulfilled orders follow the channel's status and never reserve
stock. While a connection is in shadow, an order the
channel reports as shipped is closed as
dispatched_elsewhere (Customer migration); once the
connection is active, only Vervoro's own dispatch closes an
order.
Purchase order:draft →
approved → sent →
partially_received → received;
draft or approved →
cancelled.
Vendor PO:new → in_review
→ approved (accepted quantities and prices fixed) →
acknowledged (sent through the outbox) →
reserved → dispatched → closed.
An all-rejected PO is acknowledged and closed with no reservation. An
Amazon cancellation releases its reservations.
One standard way to move a seller onto Vervoro from whatever they use
today: an off-the-shelf system (Linnworks, Veeqo, Helm, Cin7,
StoreFeeder, Mintsoft, Zoho Inventory), custom in-house software,
spreadsheets, or nothing at all. Keep it simple (D-058): move the
current state once, check it in shadow mode, then
switch over.
Principles
Current state, not history. Products, stock,
suppliers, open orders and open POs move. Order and invoice history
stays in the old system or the customer's own exports.
The channels are the source of truth for listings
and marketplace orders. Vervoro reads them itself; marketplace orders
are never imported from the old system.
One stock writer. The old system's stock sync is
switched off before Vervoro's is switched on (rule E6).
The customer's data is handled under the data processing
agreement. Their files never go into AI tools, and are deleted
after import.
Steps
Discovery call (30–60 minutes):
channels and accounts;
warehouses and locations;
number of SKUs; kits, bundles and packs;
whether they track batches and best-before dates;
open POs; Vendor and FBA use;
anything the old system does that must keep working.
Get the data into Vervoro's templates. Vervoro
has importers only; it never reads another system's
format. The data reaches the templates in one of three ways:
by hand, from the old system's own CSV or Excel exports (most
customers);
for custom in-house software, an approach still to be designed (see
Source systems);
Record the exact steps for each source in the Source systems table the first time a
customer comes from it.
Import the master data into Vervoro with the
onboarding importers, against Vervoro's own templates (below):
locations, products, kits and packs, packaging levels, suppliers, B2B
customers. Create the warehouses in settings first. No stock
yet.
Every import shows a preview with errors per row,
and writes nothing until confirmed. Each run is kept as an
import_run record (Data model).
Imports can be re-run safely: records match by SKU (and supplier
code, customer reference) and are updated, not duplicated.
Each import needs the permission of what it writes:
products.import, customers.write,
purchasing.write, orders.create,
warehouses.manage, channels.mapping.write, and
inventory.receive for opening stock (Permissions).
Connect channels in shadow mode. Each channel
gets Vervoro's own authorisation (Integrations)
and starts in shadow.
Vervoro reads the listings and auto-maps them by SKU, then by the
channel's own reference (ASIN, item id, variant id). Packs and SKUs that
differ come from the SKU mappings template (it needs the connection, so
it is imported now). The customer resolves the rest in the unmatched-SKU
list.
New orders arrive. With no stock in Vervoro yet they reserve
nothing; nothing is sent to the channel. Low-stock alerts stay off until
cutover.
An order the channel reports as shipped (by the old system) is
closed as dispatched_elsewhere: no stock effect.
Shadow check (a few days, as long as the
customer needs). Done when:
every new marketplace order appears with the right lines and
quantities;
the unmatched-SKU list is empty.
Cutover (about an hour, at a quiet time):
In the old system, switch off stock sync and order import, and stop
using it for stock.
Import stock, once: a fresh stock export from the
old system, or a physical count. Open marketplace orders already in
Vervoro reserve against it.
Import or enter the open direct and B2B orders and the open
POs.
Clear the unmanaged-listings list (rule C6). Switch each connection
to active, one at a time, with a full first push, and
spot-check each channel.
Staff work in Vervoro from now on.
If something goes badly wrong in the first
days:
Switch the connections back to shadow.
Switch the old system's sync back on.
Re-enter any stock changes made in Vervoro into the old system by
hand.
Keep the old system available for at least two weeks after
cutover.
Vervoro import templates
One CSV file per template, UTF-8, with a header row. The column list
below is the contract every source writes to, so keep it versioned and
change it only by adding columns.
Template
Main columns
Notes
Locations
warehouse code, location code, type (storage,
receiving, returns,
dispatch)
Optional, imported after the channels are connected (step 4):
auto-mapping by SKU covers most listings; use it for packs (multiplier
> 1) and SKUs that differ
Suppliers and supplier codes
supplier name, contact, lead time; supplier code → sku, units per
pack
B2B customers
name, company, email, phone, addresses, VAT number
Opening stock
sku, warehouse, location, lot or batch reference, best-before date,
quantity, unit cost
Imported once, at cutover. One received movement per
row, source migration
Open purchase orders
supplier, PO number, expected date, lines (sku, quantity, unit
cost)
Open direct and B2B orders
customer, order reference, lines (sku, quantity, price)
Reserved through the inventory service on import
Not imported:
Marketplace orders, Vendor POs and FBA shipments
are read from the channels in shadow mode. An FBA shipment still waiting
to be handed over at cutover is recorded in Vervoro as usual (rule
S10).
Invoices already issued stay in the old system
until paid; Vervoro issues the new ones. At cutover, set the next
invoice, PO and collection-note numbers in Vervoro's settings so no VAT
invoice number is reused.
Users are invited, not imported.
Source systems
Fill this in the first time a customer comes from each source: how to
get the data into the templates, what is missing, and any mapping
quirks.
Source
How the templates are filled
Notes
Linnworks
to research with the first Linnworks customer
Candidate for a direct API importer
Veeqo
to research
Helm, Cin7, StoreFeeder, Mintsoft, Zoho Inventory
to research
Custom or in-house software
to design once the system is built
Vervoro still only imports its own templates
Spreadsheets
The customer fills Vervoro's templates directly
No system
Catalogue drafted from the connected channels' listings (the
workbook flow in Products);
opening stock from a physical count in the template
The listings importer is P1 (D-035)
What to build, and when
P0 (first migration):
the template importers above, with preview and row errors;
shadow mode, the unmatched-SKU flow, closing orders as
dispatched_elsewhere, and the cutover steps.
For external customers (P2, with the onboarding
wizard): template downloads, and this checklist inside the
wizard. The importers are the ones built in P0. The paid onboarding
packages (guided setup, full migration) are delivered by the Vervoro
team with the same tooling.
The first investment after the first version: direct API
importers, so customers move from competing systems without
effort. A connector that reads the old system's API (Linnworks first,
then whichever source customers come from most) and fills the same
templates, so the customer only signs in to their old system. It feeds
the same importers, so nothing else changes. Check each system's API
terms before building.