Before two founders worry about a million customers, they need to help one person book a ticket. Meet Encore, a fictional startup where small event organizers publish events and sell tickets, and customers browse and book them.

This is the first article in the From First Customer to Viral Launch series. By the end of the series, you should be able to justify architecture choices from first customer to sustained growth and viral traffic. Each article explains one constraint, compares options, and tests the smallest useful change. We will measure before changing things, tune the existing stack, and protect correctness, security, and cost.

Encore expects small, unpredictable bursts, but scarce tickets already need protection: two buyers must not get the last one. Two founders need understandable software, not imaginary capacity.

Choose a stack two people can maintain

Choose TypeScript for both frontend and backend. In the AI era, one language keeps both founders reviewing generated code in one ecosystem. Shared types and contracts help check refactors; mature tooling, documentation, and public examples support AI-assisted iteration. AI generates Go and Rust too, but cannot replace understanding runtime behavior, errors, memory, or concurrency. A second language adds learning and iteration overhead, especially Rust’s memory-ownership rules and compilation workflow. Choose either when measured backend needs or team experience justify it. Generated code still needs review and tests; types do not validate incoming requests.

With the language settled, separate the frontend from the backend API, the server receiving browser requests. Interface changes can ship without releasing booking logic, static files can use inexpensive hosting, and the API can support future clients. Both can share a repository, but versions and browser access rules need coordination. Database credentials stay server-side.

Inside that API, use a modular monolith that ships events, bookings, and payments in one deployment, with clear internal boundaries and well-defined interfaces. Modules call defined operations instead of reaching into each other’s storage. Bookings stay easier to trace and change. As the codebase and development team grow, microservices can help teams own and release different parts independently, while limiting the impact of some failures. They also add network calls and operational work. Revisit them when coordination or release bottlenecks justify that cost.

Use Fastify to handle repetitive API work. Route schemas describe request and response shapes and validate incoming data. When configured, @fastify/swagger generates OpenAPI documentation from those schemas; @fastify/swagger-ui adds interactive browser documentation. Register documentation before routes. This reduces duplication and documentation drift, not responsibility for authorization or booking correctness.

The route below delegates booking work to a service.

export function bookingRoutes(app: FastifyInstance, bookings: BookingService) {
  app.post('/bookings', { schema: bookingSchema }, async (request, reply) => {
    // Service must authorize, validate business rules, and transact.
    const booking = await bookings.create(request.user, request.body);
    return reply.code(201).send(booking);
  });
}

This sketch assumes defined types, schemas, and verified identity. Error handling and durable transactions are omitted, not optional before real customers.

This service needs reliable storage. Start with a popular SQL database such as PostgreSQL. Its transactions let related changes succeed or fail together, avoiding half-finished bookings. Make local setup repeatable with migrations (versioned database changes), sample seed data, and an isolated test database. Test the whole browser-to-API-to-database booking path.

Encore stack: TypeScript frontend calls one Fastify API containing Events, Bookings, and Payments modules; only the API accesses the separate SQL database.

What we gain, and what we give up

Gain: One backend to build, test, and operate, so two founders can ship changes quickly.

Trade-off: Backend modules release and scale together. The separate frontend still needs to stay compatible with the API.

Revisit when: Shared releases or different scaling needs become a real bottleneck.

Check: Test the same booking flow under the same load and resources. Confirm correctness and measure response times and failures; do not assume the simpler stack is faster or safer.

Earn the next architecture decision

For the API, start with a container that packages the application, runtime, and dependencies so the same package can run locally and on compatible hosts. Serverless functions can reduce infrastructure management, but provider-specific triggers and deployment integrations can create vendor lock-in. Containers offer more control over the runtime, though connections to host services can still reduce portability.

With that choice made, deploy the frontend on inexpensive static hosting, the API on a small managed container host, and SQL in a separate managed database. Check each provider’s resource limits and actual prices rather than assuming a free tier.

Record booking correctness, response times, failures, and resource use before optimizing. In the next article, we will set Encore’s first performance contract and define what fast and reliable mean to a customer.