EngineeringJun 15, 2026

Curly Engineering Notes

The Golden Architecture for Most Startups

A practical, founder-friendly default architecture for startups: Cloudflare, TypeScript, Hono, Drizzle, queues, workflows, observability, and Mastra where AI actually belongs.

A production note for teams choosing systems they can operate calmly.

Most Startups Do Not Need A Unique Architecture

Most startups do not wake up needing a unique architecture. They wake up needing the login to work, the invoice to send, the dashboard to stop lying, and the founder to stop asking why a simple feature now requires four meetings and a spiritual negotiation with the backend.

Most startups need a boring architecture with excellent taste.

They need something a small team can understand, deploy, debug, and change without asking for permission from a platform team that does not exist yet. They need enough structure to avoid chaos, but not so much structure that every feature becomes a committee. They need one language across the stack. They need clear places for business logic, data access, background work, AI workflows, and operational visibility.

The golden architecture is not the architecture that handles every future. It is the architecture that handles the next eighteen months without painting the company into a corner.

My default version today is Cloudflare, TypeScript, Hono, Drizzle, a relational database, object storage, queues, workflows, typed contracts, and Mastra for AI features that deserve an agent or workflow layer.

Not because those names are fashionable. Fashionable stacks age like milk when the first real customer asks for a refund flow.

The reason is simpler: they create a clean operating model. Requests enter one envelope, business rules live in one place, data stays explicit, slow work moves out of the request, and AI gets a boundary instead of becoming a mist over the whole codebase.

The Shape

The system has five layers. Not because five is a sacred number. Because five is roughly where a small team can still point at the drawing and say, "yes, I know where that change belongs."

Startup system layers
Generated editorial illustration of layered startup system architecture with web, API, service, data, async, file, AI, and observability surfaces.
Every kind of work has a home: API boundary, services, data, async work, files, AI, and observability.

The client talks to the API. The API validates and authenticates. The service layer owns business decisions. Drizzle owns database shape and typed SQL. Queues and workflows own time. R2 owns files. Mastra owns agentic behavior only when the problem is genuinely open-ended or AI-shaped. Observability follows everything.

The rule is simple: every kind of work has a home.

TypeScript Is The Operating Language

The first decision is not Cloudflare. It is TypeScript.

A startup benefits from one language across browser, API, database schema, background jobs, scripts, internal tools, and AI orchestration. Not because TypeScript is perfect. It is not. We have all met the type error that reads like a ransom note.

The point is that context switching is expensive. A small team should not translate the same domain model between frontend JavaScript, backend Go, Python agent code, YAML workflows, and SQL strings before it has product-market fit.

TypeScript gives you one mental model. Hono routes, Drizzle schema, Zod validators, generated clients, background jobs, and Mastra tools can all share the same domain vocabulary.

This does not mean every function needs heroic types. It means the important boundaries should be typed enough that bad states become hard to express.

Cloudflare Is The Production Envelope

Cloudflare is the envelope around the system. It gives the team one place to think about hosting, routing, security, static surfaces, API compute, storage edges, queues, workflows, and observability before the vendor spreadsheet becomes its own product.

Use Pages for static product surfaces, marketing pages, docs, and application shells. Use Workers for the API. Use the platform's security controls before requests hit your code. Use queues when work can happen after the response. Use Workflows when the work has durable business steps. Use R2 for files. Use KV carefully for global read-mostly configuration. Use D1 for a small relational core, or use Hyperdrive when the right database is external PostgreSQL.

The goal is not to use every Cloudflare product. The goal is to avoid the early startup pattern where the team has a hosting provider, a serverless provider, a queue vendor, an object storage vendor, an auth proxy, a cron service, a model gateway, a logging product, and a deployment system before it has a repeatable sale.

One production envelope is easier to reason about. It gives the team fewer operational dialects to learn, fewer dashboards to correlate, and fewer places for small mistakes to become cross-vendor mysteries.

Hono Is The API Boundary

Hono is the right default API framework for this stack because it is small, TypeScript-first, and aligned with Web-standard runtimes. It works naturally on Cloudflare Workers without pretending the edge is a Node server.

The API layer should stay thin.

It should parse the request, validate inputs, apply authentication and authorization, call a service, and shape the response. It should not contain pricing rules, commission logic, booking policy, subscription state transitions, or AI decisions. Those belong in services.

The route handler is a border crossing. It checks passports. It does not run the country. The moment it starts running the country, someone will add tax logic next to JSON parsing and call it "just temporary."

A good route reads like this:

app.post("/projects/:projectId/invitations", async (context) => {
  const actor = await requireActor(context);
  const params = invitationParamsSchema.parse(context.req.param());
  const body = createInvitationSchema.parse(await context.req.json());

  const invitation = await invitationService.createInvitation({
    actor,
    projectId: params.projectId,
    email: body.email,
    role: body.role,
  });

  return context.json({ invitation }, 201);
});

The interesting work is not in the route. That is the point. A thin route gives you a place to enforce boundaries without turning HTTP plumbing into the product's brain.

Services Own Business Meaning

The service layer is where the product becomes legible.

There should be a service for invitations, subscriptions, billing events, referrals, reservations, documents, workflows, or whatever the business actually does. A service should speak in business verbs. createInvitation, approveCommission, startTrial, cancelReservation, publishDocument, reconcilePayment.

This layer should not import React. It should not know about button states. It should not parse HTTP requests. It should not directly know which queue provider exists unless the queue is part of its dependency interface.

Good services make the codebase easier to discuss with non-engineers. When a founder asks, "What happens when a partner refund arrives after payout?" the answer should be visible in a service function, not scattered across route handlers, webhook callbacks, dashboard components, and cron scripts like a treasure hunt nobody asked to join.

Drizzle Keeps The Data Layer Honest

Drizzle is a strong default because it keeps SQL visible while giving TypeScript real information about the schema.

The schema should live close to the app. Migrations should be explicit. Query helpers should be small. Avoid hiding Drizzle behind a generic repository for every table. That usually creates ceremony without clarity. Use repositories or data-access modules where the domain deserves a boundary, not because a diagram said so.

For most startup systems, the clean pattern is:

  • schema defines tables and relations.
  • queries hold reusable database reads and writes.
  • services call queries as dependencies.
  • tests use either isolated query tests or service tests with a controlled database.

Do not let components call Drizzle. Do not let route handlers build complex queries inline. Do not let every feature invent its own data style.

The database is where many startup architectures quietly rot. Drizzle helps because the schema and the code stay in conversation. That conversation matters. Otherwise the schema becomes archaeology and every migration feels like opening a sealed room.

Queues Let The Product Breathe

Every production app has work that should not block the request.

Emails, webhook fan-out, document processing, enrichment, notification delivery, AI summarization, analytics writes, image processing, and third-party syncs should usually move through a queue. The request records intent. The queue handles follow-through.

This one decision makes the app feel more reliable because the user-facing path stays short. It also makes the business safer because retries become explicit.

The service should decide that work needs to happen. The queue should carry it. The worker should perform it idempotently.

Idempotency is non-negotiable. If a queue message retries, the system should not send five invoices, create five payouts, or notify the customer five times. Every job that touches money, email, inventory, or external systems needs a stable key and a safe retry story. "Oops, we paid twice" is not the kind of customer delight anyone meant.

Workflows Are For Time

Queues are for work. Workflows are for time.

A startup often starts with cron scripts and queue chains because they are easy. Then the business asks for trial expiration, subscription dunning, partner commission approval, onboarding sequences, refund windows, scheduled reports, and multi-step AI review flows. Suddenly time is everywhere, and the "quick cron" has become a small legal department with a timestamp.

Put durable business processes in workflows.

A workflow should read like the business process it represents. Start trial. Wait three days. Send check-in. Wait four days. If payment exists, stop. If not, send reminder. Downgrade on day fourteen. Record every step.

That is easier to reason about than a pile of scheduled functions that each know one fragment of the story.

Mastra Belongs Where AI Has Agency

Do not put an agent everywhere. This sentence should probably be printed and taped above a few whiteboards.

Mastra is useful because it gives TypeScript teams a structured way to build agents, tools, workflows, memory, and observability. Its own docs describe agents as systems that use LLMs and tools to solve open-ended tasks, while workflows are better when the steps are known in advance.

That distinction matters.

Use normal code when the rules are known. Use a workflow when the steps are known but time, retries, or state matter. Use an agent when the task is open-ended enough that the system must decide which tool to call, what information to inspect, and when it has enough confidence to stop.

Good startup uses for Mastra tend to involve ambiguity. Support triage can inspect account state and propose a reply. Sales research can gather context before a founder call. Internal operations assistants can summarize messy records. Document review can let the model use typed tools and return structured output. Multi-step AI features can use memory, tool calls, and observability without scattering prompt logic across the app.

Bad uses tend to involve rules that already have a right answer. Do not replace a simple SQL query. Do not replace a deterministic billing rule. Do not let a model approve money movement without review. Do not create an agent because the feature sounded boring without one.

The agent should be a guest in the architecture, not the architecture itself.

Typed Tools Are The AI Boundary

If Mastra agents can act, their tools need the same discipline as public APIs.

A tool should have a clear name, a narrow purpose, a Zod schema, authorization checks, structured output, and logging. It should call services, not reach directly into random tables. It should be boring enough that a human can audit what the model was allowed to do.

This is how AI stays inside the product's rules.

The agent can decide to call findCustomerOrders, summarizeSupportHistory, or draftRefundExplanation. It should not get a raw database connection and a motivational prompt. That is not autonomy. That is giving a stranger the keys and admiring your own confidence.

Treat every agent tool as a capability you are granting. If the capability would be dangerous in a junior employee's hands, it is dangerous in a model's hands.

Contracts Keep Frontend And Backend Calm

The frontend should not guess response shapes.

Use typed contracts between the client and API. That can be OpenAPI, RPC-style typed clients, shared Zod schemas, or another disciplined pattern. The exact tool matters less than the rule: the client and server should not drift silently.

For a startup, drift is expensive because it creates distrust between product surfaces. The dashboard shows one state, the email says another, the webhook stored a third, and support becomes the reconciliation layer. Nobody planned for support to become a distributed systems debugger, but here we are.

Shared contracts prevent some of that. Not all of it, but enough to matter.

Observability Is A Product Feature

Logs, traces, metrics, job histories, workflow events, and AI traces are not internal luxuries. They are how the team answers customer questions.

What happened to this signup? Why did this payout pause? Did the webhook arrive? Did the model call fail? Which tool did the agent use? Why did the queue retry? Which version of the workflow ran?

Those questions appear earlier than founders expect. Usually right after the first customer whose problem cannot be solved by looking at the happy-path dashboard.

Add request ids. Add job ids. Add workflow ids. Add actor ids. Add tenant ids. Add provider ids. Do not log secrets. Do not log private content casually. But design the system so a human can reconstruct a story.

A startup with good observability feels larger than it is. A startup without it feels like everyone is guessing in a very expensive room.

The Folder Shape

The repository can stay simple. The goal is not to build a monastery of packages. The goal is to make the obvious place actually obvious.

apps/
  web/
  api/
packages/
  db/
  domain/
  contracts/
  ui/
  mastra/

apps/web owns the product interface. apps/api owns the Hono API and Cloudflare bindings. packages/db owns Drizzle schema and migrations. packages/domain owns services and business rules. packages/contracts owns shared API shapes. packages/ui owns reusable interface pieces. packages/mastra owns agents, workflows, tools, and AI observability glue.

Keep imports directional. Apps can import packages. Packages should not import apps. UI should not import database code. Agents should call tools that call services. Services should not depend on route handlers.

This is enough architecture for most startups.

What I Would Build First

Start with the smallest production skeleton that preserves the right boundaries.

  1. A Pages or web app surface.
  2. A Hono API on Workers.
  3. Auth at the API boundary.
  4. Drizzle schema and migrations.
  5. One service module for the core business object.
  6. R2 only if files exist.
  7. A queue for slow side effects.
  8. Structured logs from the beginning.
  9. A single Mastra workflow or agent only if the product truly needs AI.

Do not add microservices. Do not add Kafka. Do not split the database by bounded context. Do not build an internal platform. Do not create a generic repository layer for every table. Do not create an agent layer for deterministic workflows.

The first architecture should help the team move, not prove that the team has read architecture books. It should make the common path obvious and the dangerous path visible.

Where This Architecture Breaks

No default survives every company.

This stack may be wrong if the product needs heavy regional data residency, complex OLAP from day one, low-level network control, long CPU-bound jobs, specialized GPU workloads, unusual database extensions, or enterprise deployment into customer clouds.

That is fine. A golden architecture is not a universal architecture. It is a strong default.

The test is whether the architecture makes common work easy and uncommon work possible. For most startups, this version does.

The Judgment

The golden architecture is not Cloudflare plus TypeScript plus a list of libraries.

It is a way of keeping the system honest while the business is still changing its mind, which it will. Often. Sometimes before lunch.

The edge handles requests. Hono keeps the API small. Services hold business meaning. Drizzle keeps data explicit. Queues protect the request path. Workflows carry time. R2 stores objects. Contracts prevent drift. Observability tells the story. Mastra gives AI a disciplined place to live when AI is actually part of the product.

That is enough.

Most startups do not need a unique architecture. They need a calm one that lets them find out what business they are really building.