This year we shipped two very different web platforms with the same method. One is a multi-tenant storefront platform for a Romanian retail group: Laravel, one database per tenant, three admin panels, a public shop per brand. The other is a partner and payout platform for a US client: three Next.js applications on AWS App Runner, a TypeScript API, DynamoDB and PostgreSQL side by side, money moving through it every month.
Different stacks, different countries, different budgets. The way we build them is identical, and it is what this article is about. Not "we are agile" and not a tool list, but what actually happens in the first three weeks and what the client can expect in the year after.
Week 0: discovery that ends in a repository
Most briefs we receive are a paragraph and a deadline. Discovery is one or two paid weeks that turn that paragraph into three things: a data model, a list of environments, and a first milestone that is small enough to be real.
The data model is the part people underestimate. In the storefront platform, the decision that every tenant gets its own database was made in discovery, on a whiteboard, after asking one question: will a tenant ever need to leave with their data? The answer was yes, so isolation had to be physical, not a tenant_id column. That single decision shaped the ORM setup, the admin panels, the backup strategy and the pricing page.
In the partner platform the equivalent question was: which access patterns are fixed forever? Order lookups by partner and month never change, so they went into DynamoDB with a single-table design. Everything reporting-shaped, where the questions change every quarter, went into PostgreSQL. We wrote about that choice in DynamoDB single table or Postgres: how we chose.
Discovery ends when there is a repository with a README, an empty CDK stack (or a Laravel skeleton), and a milestone the client can describe in one sentence. If it ends with a slide deck instead, it was a sales call.
Week 1: the skeleton is in production before the first feature
The first thing we build is not a feature. It is the boring frame every feature will hang on: authentication, roles, the three environments, a CI pipeline that deploys on merge, and a preview environment per pull request. By the end of week one there is a URL the client can log into. It does nothing useful yet, and that is the point: every piece of plumbing that could delay a launch has already been exercised.
For the partner platform this meant a CDK codebase with three AWS accounts and GitHub Actions deploying through OIDC roles, no long-lived keys anywhere. We described the setup in three AWS accounts, one CDK codebase. For the storefront platform it meant a Laravel skeleton with the tenancy package wired, a super-admin panel, a workspace panel per tenant, and a deploy script that creates a tenant database in one command.
The reason we insist on this order is simple: the skeleton is where the surprises live. Build-time versus runtime configuration in Next.js, cookie domains across subdomains, a health check that reports healthy while the database is unreachable. We would rather meet those in week one, on a page that says "hello", than in the week of the launch.
Choosing the stack: TypeScript by default, Laravel when it earns it
We are a TypeScript-first team. A Node.js API, a React or Next.js frontend and PostgreSQL share one language, one linter, one test runner and one reviewer. For a team of two or three that is a real speed advantage, and typed contracts between frontend and backend remove a whole class of bugs before they exist.
We use Laravel when the project is a better fit for it, and the storefront platform was. It needed three admin panels with dozens of CRUD screens, per-tenant databases, queues, scheduled jobs and a CMS-shaped content layer. Laravel with Filament gave us the admin panels in days instead of weeks, and the tenancy package handled database switching, migrations per tenant and asset isolation without us writing a framework. Choosing Next.js there would have meant building an admin framework before building the product.
The rule we apply: pick the stack that makes the second year cheap, not the first demo fast. Then write the reason down, so nobody re-litigates it in month six.
Data: boring by default, exotic on purpose
PostgreSQL is the default. It is the database that will still be understood by whoever inherits the system, and it handles the reporting questions nobody could predict during discovery. We reach for DynamoDB only when the access patterns are fixed, the scale justifies it, and someone on the team is willing to own the single-table design.
Two lessons from this year worth passing on. First, when money is involved, lock every rate at the moment of the transaction. The partner platform converts orders from several currencies; an early version applied the current exchange rate at payout time instead of the rate on the order date, and a partner noticed a discrepancy on their statement. We fixed the calculation, backfilled the history, and now the rate is stored on the order row and never recomputed. Second, when using a database over an HTTP data API, expect JSON columns to arrive as strings. A five-line helper saved us from a dozen scattered JSON.parse calls and the bugs that come with them.
Weeks 2 to 12: increments you can see
From week two, the rhythm is fixed. Every pull request gets a preview environment, so review happens on a running application rather than on a diff. Every Friday there is a demo on staging of what changed that week. Every Monday the client gets a report of hours spent, per task, with the next week's plan. The backlog is theirs to reorder at any time.
This is where the hourly, time-and-materials model earns its keep. There is no fixed scope to defend, so when the partner platform's client asked mid-project for a data feed their analysts could consume in their BI tool, we did not open a change request. We estimated it, they prioritised it, and it shipped two weeks later.
The things that bite, and how we build around them
Long-running requests. The partner platform exports statements as CSV and XLSX. The first version buffered the whole file and the proxy in front of App Runner closed the connection at 120 seconds on large months. Now CSV is streamed row by row so the connection never idles, XLSX is generated in a worker and delivered by link. We covered the platform's other quirks in App Runner after six months.
Build-time versus runtime configuration. Next.js inlines public variables at build time. A deploy that only changed an environment variable did nothing, and it took an afternoon to understand why. The build now takes its configuration explicitly per environment, and the deploy script refuses to reuse an image tag for a different configuration. Details in Next.js build-time vs runtime config on containers.
Monorepos that rebuild everything. Three applications and a shared package in one repository meant every commit rebuilt three images. We changed the pipeline to build only what changed, which cut CI time by more than half. See monorepo: three apps, build only what changed.
AI-assisted development. We use Claude Code daily and it writes a real share of routine code, tests and infrastructure. It does not decide the data model, the security boundaries or what gets deployed; a senior engineer reviews every line. We were specific about the split in what Claude Code wrote of our infrastructure, and what we refused.
The year after
A web application is a running system, not a delivery. When the build phase ends, two things happen. We hand over documentation that a new engineer can start from: architecture notes, decision records, the runbook, and the deploy procedure. And the client chooses between a clean handover to their own team, or keeping us on for a few hours a month to operate and extend.
Both of this year's platforms chose the second. The storefront platform gets new modules every month, most recently a product tax module and dynamic admin themes. The partner platform gets a weekly stream of small fixes and features, triaged from the client's ticket board, each one verified in production before it is closed. The engineers are the same people who did discovery in week zero, which is the whole point.
If you have a product to build and want it done this way, talk to us. The first conversation is free; discovery is where the clock starts.