Skip to content

One platform, two trades, and three quarters of the domain never changed

A multi-tenant Laravel platform sold into two trades. Purchasing, sales, returns, expenses, tenancy and roles are one product; only the nouns on top of them change.

Dashboard

Harbor Street Pharmacy · Tuesday 09/15/2026 · the dashboard every trade gets

New purchase orderSearch this shop
SpinePurchasing, sales, returns, expenses, banks, roles, shops, tenancy · built oncePharmacyMedicines, generics, batches, stocked units
Scripts waiting
38across today's intake
In the queue
8being worked now
Sales today
$3,918.40
Expiring ≤ 30 days
14
Purchase orders open
31 part received

Dispensing queueSpine

8 deep · 38 scripts waiting
Received3Checked2Filling2Ready1Stages named by the trade · queue from the spine
RX-40211Amoxicillin 500 mg caps × 21M. Alvarez · Dr. OkaforReady4 min
RX-40214Lisinopril 10 mg tabs × 90R. Chen · Dr. PatelFilling7 min
RX-40215Metformin 850 mg tabs × 60J. Brooks · Dr. PatelFilling9 min
RX-40217Salbutamol inhaler 100 mcg × 2A. Novak · Dr. LindqvistGeneric offeredChecked11 min
RX-40218Atorvastatin 20 mg tabs × 30T. Moreau · Dr. OkaforChecked12 min
RX-40220Sertraline 50 mg tabs × 30K. Ibrahim · Dr. HaleReceived14 min
RX-40221Omeprazole 20 mg caps × 28L. Fischer · Dr. HaleGeneric offeredReceived15 min
RX-40223Levothyroxine 75 mcg tabs × 90S. Duarte · Dr. LindqvistReceived18 min
30 more scripts waiting behind the queue

Low stockSpine

StockLevel · spine
Item · Batch · expiryOn handReorder at
Amoxicillin 500 mg capsB24-1187 · 10/31/202684 caps200
Salbutamol inhaler 100 mcgS7-0442 · 03/31/20276 each12
Omeprazole 20 mg capsOM-3310 · 10/12/2026112 caps280
Levothyroxine 75 mcg tabsLV-0917 · 06/30/2027150 tabs360
Cetirizine 10 mg tabsCT-5521 · 01/31/202740 tabs120

Sales todaySpine

$3,918.40
91011121234
S-104123 items · card$46.8511:42 AM
S-10411Script RX-40209 · copay$12.5011:38 AM
S-104105 items · cash$31.2011:31 AM

Who, what, and how long

Industry

E-commerce & Retail

Duration

30 weeks

Cooperation model

Dedicated team

Services
Platform engineeringMulti-tenancyVertical configuration
Integrations
Payment gatewaySMS providerTransactional emailS3-compatible storage
Technologies
LaravelPHPMySQLLivewireSpatie PermissionDataTables
Team
1 Project lead3 Backend engineers1 Frontend engineer1 QA engineer

Client name withheld under NDA. Engagement details are shown to the extent our agreement permits.

The problem

The hard problem

Two verticals had been quoted as two products, which meant two codebases, two release trains, and a bug fixed twice or, more often, once. The commercial pull was toward a third vertical, and the engineering reality was that it would cost as much as the first two combined.

Two codebases meant a bug was fixed twice or, in practice, once. The pharmacy fork had received a fix to purchase-return arithmetic that the restaurant fork never got, and the restaurant fork had a stock-transfer fix the pharmacy lacked. Neither team knew, because there was no mechanism by which either would have found out.

A single tenanted platform where the shared spine is the product. Purchasing, sales, returns, expenses, banks, roles and shops live once; the vertical contributes its own entities and its own vocabulary and nothing else.

How the pieces fit

  1. 01

    Diffed the two domains honestly and found three quarters of the model layer identical

    The two domain models were diffed entity by entity before any consolidation, which put roughly three quarters of the model layer in common and settled the argument.

  2. 02

    Made tenancy the boundary: one deployment, isolated data per shop

    Retrofitting tenancy meant migrating two populated databases into one, which is where most of the risk in the engagement actually sat.

  3. 03

    Kept purchasing, sales, returns and expenses in the spine, where both trades agree

    Purchasing, sales, returns, expenses and banks live in the spine, where both trades genuinely agree, instead of being duplicated with different field names.

  4. 04

    Let each vertical add its own entities without touching the shared ones

    Vertical-specific fields are extensions on the shared item, so the pharmacy gets batches and expiry and the restaurant never sees them.

  5. 05

    Put roles and permissions in the spine so an owner, a manager and a register operator mean the same thing in both trades

    Roles and permissions sit in the spine, so a manager means the same thing in both trades and a third vertical inherits the model instead of inventing one.

Introduction

The system we were asked to build

The software vendor sells the same back-office to pharmacies and to restaurants. We built it once (tenancy, roles, purchasing, sales, returns and expenses) and made the trade-specific part a thin layer on top.

A software vendor selling the same back-office to pharmacies and to restaurants, quoted and built as two products by two teams. The commercial pull was toward a third vertical, and the engineering estimate for it was roughly the sum of the first two. That was the moment someone finally asked how different they really were.

Platform Architecture & Engineering

A spine, not a fork

Purchasing, sales, returns, expenses, banks, roles, shops and tenants are built once and shared by every vertical.

Purchasing, sales, returns, expenses, banks, roles, shops and tenancy are built once and shared, because they're the same in every trade, whatever the trade insists. A new vertical contributes its own entities and its own vocabulary and inherits the rest. That's the difference between a second product and a second copy of the first one that has to be maintained forever.

What shipped
  • Eight shared subsystems built once, inherited by every vertical
  • A vertical contributes entities and vocabulary, nothing else
  • No fork to maintain in parallel
Dashboard

Juniper & Salt · Tuesday 09/15/2026 · the dashboard every trade gets

New purchase orderSearch this shop
SpinePurchasing, sales, returns, expenses, banks, roles, shops, tenancy · built onceRestaurantFoods, ingredients, covers, sections, allergens
Covers booked tonight
1423 sections
In the kitchen
12being worked now
Sales today
$6,284.10
Allergen flags open
5
Purchase orders open
21 part received

Kitchen queueSpine

12 tickets · 31 covers seated
Fired3Cooking2Plating2Ready1Stages named by the trade · queue from the spine
T-12 · 4 coversShort rib, halibut × 2, risottoPatio · server AnaReady2 min
T-07 · 2 coversDuck breast, burrataMain room · server TheoNut allergyPlating6 min
T-15 · 6 coversTasting menu × 6Main room · server AnaPlating8 min
B-03 · 2 coversFries, crudo, olivesBar · server KiraCooking9 min
T-04 · 3 coversGnocchi × 2, chickenMain room · server TheoGluten freeCooking10 min
T-18 · 2 coversHalibut, beet saladPatio · server KiraFired12 min
T-09 · 4 coversShort rib × 2, risotto × 2Main room · server AnaShellfishFired13 min
B-05 · 1 coverBurger, side saladBar · server KiraFired14 min
4 more tickets fired behind these

Low stockSpine

StockLevel · spine
ItemOn handReorder at
Heirloom tomatoes8 lb30
Arborio rice11 lb25
Halibut fillet6 lb18
Unsalted butter9 lb20
Burrata7 each24

Sales todaySpine

$6,284.10
121256789
S-22871T-03 · 4 covers · card$218.408:14 PM
S-22870B-01 · 2 covers · card$64.758:09 PM
S-22869T-11 · 2 covers · cash$97.008:02 PM
On screen

The same dashboard component under a restaurant. The queue, stock and sales panels and the spine half of the rail are identical; kitchen stages, covers, sections and allergen flags are the only things that belong to the trade.

Purchase order PO-2026-0418

Purchasing · the spine's purchase order, with the pharmacy's extension columns

PrintReceive remainingSearch this shop
PO-2026-0418Meridian Wholesale DrugPart received
Ordered
09/08/2026
Receipt GR-3307
09/11/2026
In full
7 of 9
Order value
$445.80
Received value
$391.20

LinesSpine

9 lines · batch and expiry recorded at receipt
Shared itemPharmacy extensionSpine purchasing
ItemBatchExpiryOrderedReceivedUnitLine
Amoxicillin 500 mg capsgeneric · 3 brand equivalentsB24-120408/31/20275005000.09$45.00
Omeprazole 20 mg capsgeneric · 2 brand equivalentsOM-334205/31/20275602800.12$67.20
Lisinopril 10 mg tabsLS-209812/31/20279009000.04$36.00
Salbutamol inhaler 100 mcgS7-046102/28/202824247.85$188.40
Metformin 850 mg tabsgeneric · 1 brand equivalentMF-771011/30/20276006000.03$18.00
Sertraline 50 mg tabs——30000.07$21.00
Cetirizine 10 mg tabsCT-558010/31/20263603600.02$7.20
Levothyroxine 75 mcg tabsLV-095009/30/20277207200.05$36.00
Atorvastatin 20 mg tabsAT-447101/31/20284504500.06$27.00
1 line part received · 1 line awaiting the supplierReceived $391.20 of $445.80

Item model

where these columns come from
items · spine
skunamecategory_idunit_idtax_rate_id
item_pharmacy · extension
batch_noexpiry_dategeneric_ofstocked_unit_id

A restaurant tenant’s items have no extension row. These four fields never reach its screens.

Against this order

GR-3307 · received 09/11/20267 lines in full, batches recorded
Omeprazole 20 mg caps280 of 560 received · rest to follow
Sertraline 50 mg tabs300 due · waiting on the supplier

Batch and expiry where it matters

On screen

One purchase order, part received. Ordered, received and costs are the spine's purchasing columns; batch, expiry and generics come from a pharmacy extension on the shared item, and a restaurant tenant's items have no such row.

The pharmacy gets batches, generics and stocked units; the restaurant never sees them.

The pharmacy needs batch numbers, expiry dates, generic equivalents and stocked units; the restaurant would be actively worse for having them on screen. Vertical-specific fields are modeled as extensions on the shared item, so each trade sees its own vocabulary and neither pays for the other's complexity.

What shipped
  • Vertical fields modeled as extensions on the shared item
  • Batches, expiry and generics exist only where they belong
  • Neither trade pays for the other's complexity

Tenancy as the boundary

One deployment, isolated data per shop, and onboarding that isn't a release.

One deployment with data isolated per shop, scoped in the query layer instead of by convention, so a new tenant is a record and an onboarding flow. No release required. That's what made the second vertical viable at all: the cost of a new customer stopped being an engineering cost and became a support one.

What shipped
  • One deployment, per-shop isolation enforced in the query layer
  • Onboarding a tenant is a record, not a release
  • New customers cost support time, not engineering time
Provision a vertical · Dental

The third vertical, on the release every tenant already runs

No deploy requiredTue 09/15/2026
Modules
11 8 inherited · 3 new
Roles & permissions
Inherited 5 roles
Release
r2026.09.2 same as every tenant
Deployments needed
0
Vs fork-model estimate
−70% engineering time

Modules for dental

8 of 11 arrive inherited
ModuleSource
PurchasingOrders, receipts, supplier invoicesSpine
SalesSales, payments, invoicesSpine
ReturnsPurchase and sale returns as documentsSpine
ExpensesExpenses and recurring costsSpine
BanksAccounts, transfers, reconciliationSpine
RolesFive roles, the whole permission modelSpine
ShopsShops, items, stockSpine
TenantsIsolation, settings, onboardingSpine
PatientsExtends CustomerDental
Treatment plansExtends SaleDental
RecallsNew entity, scoped per tenantDental
New modules extend shared entities; none edits them

Onboarding · Brightline DentalDental

tenant #312
1Create tenant recordtenants #312 · Brightline DentalDone
2Attach verticaldental · 8 inherited, 3 new modulesDone
3Per-tenant settingsSales tax, fiscal year, receipt footerDone
4Seed roles from the catalogue5 roles · spine and dental permissionsDone
5Invite the ownerDr. Priya Raman · sent 10:12 AMWaiting on owner
6Import the practice's price listAfter the owner signs inNext
Every step writes a record. None of them is a release.

Tenant scope

applied in the query layer on every read
select * from items where tenant_id = 312 and …
#118Harbor Street PharmacyPharmacysince 03/02/2026
#204Juniper & SaltRestaurantsince 05/18/2026
#312Brightline DentalDentalsince 09/15/2026
On screen

Provisioning the third vertical: eight of eleven modules and the whole roles model arrive inherited, three modules are new to dental, the first practice's onboarding is a list of records, and every read is scoped to its tenant in the query layer.

Purchase return PR-0207

Returns · its own document with its own ledger entries; the receipt it returns against is untouched

Print credit requestSearch this shop
PR-0207Northfield Produce Co.Posted 09/02/2026
Against receipt
GR-2291 08/29/2026
Purchase order
PO-1184
Lines returned
2 of 7 · partial
Credit due
$51.90 at invoiced prices

Returned linesSpine

PurchaseReturnLine
ItemReceivedReturnedInvoiced atPrice todayCredit
Heirloom tomatoesSplit and soft on arrival40 lb12 lb$3.40$2.95$40.80
Romaine heartsWilted outer leaves24 ct6 ct$1.85$1.85$11.10
Partial quantities on 2 lines$51.90
Tomato price changed to $2.95 on 08/31; credited at the $3.40 it was invoiced at

Ledger entriesSpine

posted 09/02/2026 · Sep 2026 period
AccountMemoDebitCreditStock
Payable · NorthfieldCredit due on PR-0207$51.90——
Inventory · produceHeirloom tomatoes—$40.80—
Inventory · produceRomaine hearts—$11.10—
Stock movementHeirloom tomatoes——−12 lb
Stock movementRomaine hearts——−6 ct

Receipt GR-2291

received 08/29/2026
Unchanged by the return · stays in Aug 2026
LineQtyAmount
Heirloom tomatoes · 12 on PR-020740 lb$136.00
Romaine hearts · 6 on PR-020724 ct$44.40
Basil10 bunch$16.00
Shallots15 lb$33.00
Lemons60 ct$27.00
Fingerling potatoes30 lb$57.00
Flat-leaf parsley12 bunch$15.00
Receipt total$328.40

By period

Purchasing report
Aug 2026Purchases · GR-2291as received, never restated$328.40
Sep 2026Purchase returns · PR-0207posted when the goods went back−$51.90
No status was flipped on the receipt to get here.
On screen

A partial purchase return as its own document: two of seven receipt lines, credited at the invoiced price after a price change, with its own ledger entries in September while the August receipt it returns against stays unchanged.

Returns modelled properly

Purchase and sale returns are their own entities, never a status on the original, so the ledger stays honest.

A purchase return and a sale return are their own entities with their own documents and ledger entries. That keeps the ledger honest: the original sale still happened and still reports as revenue for its period. It also means a partial return, a return at a different price, and a return after a price change all model correctly and leave the source record intact.

What shipped
  • Returns are their own entities with their own ledger entries
  • The original transaction stays intact for its period
  • Partial and price-changed returns model correctly

One permission model

A manager means the same thing in both trades, and the third vertical inherits it for free.

One permission model through Spatie, defined against the shared spine, so manager means the same thing in a pharmacy and a restaurant, and a third vertical inherits it without a new matrix being invented. Permissions are checked server-side per action, never by hiding buttons, and a vertical can add its own without redefining the ones it shares.

What shipped
  • One role model across every vertical
  • Checked server-side per action, not by hiding controls
  • Verticals add permissions without redefining shared ones
Roles & permissions

5 roles across 8 permissions · defined in the spine, checked on the server per action

Assign roleSearch this shop

Permission matrix

Harbor Street Pharmacy
PermissionOwnerManagerSupervisorStock clerkTill operator
Spine · the same in every trade
Raise purchase orderpurchasing.order.create—
Approve purchase returnreturns.purchase.approve———
Issue sale returnreturns.sale.create—
Record expenseexpenses.record——
Reconcile bank accountbanks.reconcile———
Pharmacy · added by the vertical
Release script to counterpharmacy.script.release—
Substitute a genericpharmacy.generic.substitute——
Write off expired batchpharmacy.batch.write_off———
Won't commit without a second signatureGranted to the role

The same five roles in every tenant

Harbor Street PharmacyPharmacyOwner · Manager · Supervisor · Stock clerk · Till operator
Juniper & SaltRestaurantOwner · Manager · Supervisor · Stock clerk · Till operator
Brightline DentalDentalOwner · Manager · Supervisor · Stock clerk · Till operator

Checked on the server

this morning
10:41Kevin Tran · Till operatorreturns.sale.createAllowed
10:38Kevin Tran · Till operatorbanks.reconcileDenied · 403
10:36Dana Whitlock · Managerpharmacy.batch.write_offNeeds 2nd signature
10:31Sofia Reyes · Stock clerkpurchasing.order.createAllowed
10:27Sofia Reyes · Stock clerkreturns.purchase.approveDenied · 403
10:22Dana Whitlock · Managerreturns.purchase.approveSigned by owner
10:18Ravi Anand · Supervisorpharmacy.generic.substituteAllowed
10:12Kevin Tran · Till operatorpharmacy.generic.substituteDenied · 403
10:05Ravi Anand · Supervisorexpenses.recordAllowed
9:58Kevin Tran · Till operatorpharmacy.script.releaseAllowed
A hidden button isn't a check. Every action is.
On screen

Five roles across eight permissions, the spine's banded above the pharmacy's, a key on the two actions that won't commit without a second signature, and this morning's server-side checks allowing, refusing and holding individual actions.

Process

Phase by phase

  1. Phase 1: The Diff

    Finding Out How Different They Actually Were

    Compared the two domains model by model. The spine (purchasing, sales, returns, expenses, banks, roles, shops, tenants) was identical. Only a quarter of the entities were trade-specific.

    • Domain Diff
    • Shared Spine Definition
    • Vertical Boundary
  2. Phase 2: Tenancy

    One Deployment, Isolated Shops

    Made the tenant the boundary so a shop's data is isolated without a shop's code being separate, and onboarding a new customer stops being a deployment.

    • Tenant Isolation
    • Onboarding Flow
    • Per-Tenant Settings
  3. Phase 3: The Spine

    Built Once, For Both

    Built purchasing, sales, returns and expenses as the shared core, with the double-entry between purchase and return modeled as real documents instead of a status flag.

    • Purchasing & Returns
    • Sales & Returns
    • Expenses & Banks
  4. Phase 4: The Verticals

    A Quarter Of The Domain

    Added medicines, generics, batches and stocked units for the pharmacy; foods, ingredients and covers for the restaurant. Neither vertical can reach into the other.

    • Pharmacy Entities
    • Restaurant Entities
    • Vertical Vocabulary
  5. Phase 5: Roles Everywhere

    One Permission Model

    Put roles and permissions in the spine so a manager means the same thing in both trades, and a new vertical inherits the whole access model instead of inventing one.

    • Role Catalogue
    • Permission Checks
    • Per-Tenant Roles
Model registry

Every model-layer entity in the merged platform, and which verticals it serves

Export diffTue 09/15/2026
Spine entities
46 8 subsystems
Pharmacy only
9
Restaurant only
8
Domain shared
73% 46 of 63
Codebases maintained
1
Spine · serves both 46Pharmacy only 9Restaurant only 8Measured entity by entity across both former codebases

The spineSpine

46 entities · every vertical
Purchasing7
SupplierPurchaseOrderPurchaseOrderLineGoodsReceiptReceiptLineSupplierInvoiceSupplierPayment
Sales7
CustomerSaleSaleLinePaymentPaymentMethodDiscountInvoice
Returns6
PurchaseReturnPurchaseReturnLineSaleReturnSaleReturnLineCreditNoteReturnReason
Expenses5
ExpenseExpenseLineExpenseCategoryRecurringExpenseExpenseReceipt
Banks5
BankBankAccountBankTransactionReconciliationTransfer
Shops8
ShopItemItemCategoryUnitStockLevelStockMovementStockTransferTaxRate
Roles4
RolePermissionRoleAssignmentSecondSignature
Tenants4
TenantTenantSettingVerticalOnboardingStep

Trade-specific

17 entities
Pharmacyextends Item, Sale, Customer
MedicineGenericBatchStockedUnitScriptPrescriberPatientDispenseLabelControlledEntry
Restaurantextends Item, Sale, Shop
FoodIngredientCoverSectionAllergenTableKitchenTicketModifier
On screen

The model registry from the domain diff: forty-six spine entities across eight subsystems against seventeen trade-specific ones, which is where the seventy-three percent comes from, all in one codebase.

What it carries now

73%

Domain shared across verticals

1

Codebases maintained

−70%

Cost of the next vertical

No deploy

Onboarding a shop

Domain shared is measured as the proportion of model-layer entities in the merged platform that serve both verticals. Codebases maintained is a count. Cost of the next vertical compares the third vertical's actual engineering time against the estimate produced under the fork model. Onboarding a shop is a state: it requires no deployment.

Client name withheld under NDA. Figures are approximate, drawn from the engagement’s own reporting.

What the architecture settled

  • Two vertical products usually share far more than the people selling them believe. Measure it before forking.

    The people selling two verticals describe them as different products because that's how they're sold. The model layer disagreed, and only a diff would have shown it.

  • A bug fixed in one fork and not the other is the real cost of shipping two codebases.

    The real cost of a fork is the fix that lands on one side. Both teams had them, and neither had any way to discover the other's.

  • Tenancy is what turns onboarding a customer from a deployment into a form.

    Tenancy turns a customer from a deployment into a form, which is what changes the economics of a business selling to small operators.

  • If returns are a status flag instead of an entity, the ledger will eventually disagree with the stock.

    A return modeled as a status flag corrupts the ledger quietly: the original sale has to stay intact for its period, and a flag can't express that.

How the work was run

The engagement turned on a week spent diffing the two domains before writing anything. Three quarters of the model layer was identical, which changed the plan from two products to one platform and a thin vertical layer. It was the cheapest week of the project by a wide margin.

Thirty weeks with a dedicated team, opening with an honest diff of the two domain models before anyone assumed how much they shared. That measurement (roughly three quarters identical) is what made consolidation defensible to two teams who each believed their vertical was the special one.

One fix, followed through

Two forks each got a fix the other never did. The spine gets every fix once.

The purchase-return arithmetic fix and the stock-transfer fix, followed first through the two codebases the vendor used to ship, then through the one spine every tenant runs now, and finally into a third vertical that inherits both. Switch tabs, or use the arrow keys once one is focused.

Each trade had its own copy of purchasing, returns and stock. A fix landed in whichever fork its team happened to be working in, and there was no mechanism by which the other team would have found out.

  1. Two codebases, two release trains
  2. Purchase-return arithmetic fixed in the pharmacy fork
  3. Stock-transfer fix lands in the restaurant fork
  4. Roles and permissions built twice, once per fork
  5. Nothing tells either team what the other fixed
Codebases maintained2a fix lands in one of them
What each trade’s tenants runPharmacyRestaurantDental
Purchase-return arithmetic fixHas itNever got itNot sold yet
Stock-transfer fixNever got itHas itNot sold yet
Roles & permissionsIts own copyIts own copyNot sold yet
Batches, expiry & genericsIts ownNot on screenNot sold yet
Covers, sections & allergensNot on screenIts ownNot sold yet
Patients, treatment plans & recallsNot on screenNot on screenNot sold yet

Two fixes, each landed once. Both teams had one the other lacked, and neither knew.

Why a fix now reaches everyone: the trade-specific quarter of the domain sits on top of the spine as extensions, so there is no second copy of purchasing or returns for a fix to miss. Step pacing is illustrative.

Architecture

From one shop’s request to a ledger that stays honest, in any trade

A pharmacy and a restaurant take the same path through the same code. The only stage that knows which trade it is serving is the vertical extension, and it contributes entities and vocabulary, nothing else.

  1. 01 · Source
    A shop's requestEvery shop in every trade runs on one deployment. No tenant has code of its own, so a new customer is a record, not a release.
  2. 02 · Tenancy
    Tenant scopePer-shop isolation is enforced in the query layer, never left to convention, so data stays with the shop it belongs to.
  3. 03 · Access
    Spatie permission checkChecked server-side per action, not by hiding controls. Roles live in the spine, so a manager means the same thing in every trade.
  4. 04 · Engine
    Spine + vertical extensionPurchasing, sales, returns, expenses and banks are built once; a vertical adds its own entities on top and can't reach into another vertical's.
  5. 05 · State
    MySQL ledgerPurchase and sale returns are their own entities with their own ledger entries, so the original transaction stays intact for its period.

What one shop can reach

Tenant isolation & ledger integrity

Each shop's data stays its own

Data is isolated per shop and scoped in the query layer. One deployment serves every tenant, so isolation is one code path instead of something each install has to get right.

A return never rewrites a sale

Returns are their own entities with their own documents and ledger entries. The original still reports for its period, and partial or price-changed returns model correctly.

Refused on the server, not hidden in the UI

Permissions are checked server-side per action. A vertical adds its own permissions without redefining the shared ones, so a manager means the same thing in every trade.

Selling one back office into more than one trade? Scope your build in 3 minutes.

Scope your build
Have a project?

Let's talk

Running a large platform, shaping a first MVP, or getting a product ready for a funding round? Tell us where you are. We'll shape the process around it, and stay with you after launch.