SA
shubhanshu.dev // v.2026
--:--:-- EST
← writing
Jan 15, 2026 · 6 min read

One platform, three products

How Sjocamp, Meerkat and TickUpToks share one AWS account, one database server, one design system and one SaaS template, and what that buys a solo builder.

platformawssaas

I run three SaaS products by myself: Sjocamp (waitlists and signup forms that capture verified leads), Meerkat (a scheduler for Bluesky, X and Threads) and TickUpToks (an AI canvas that turns a brand brief into TikTok slideshows). None of them has a team. What they share is a platform, and the platform is the reason there are three instead of one.

This is how it’s put together, and what I’d do the same way again.

The boring 80% is the same every time

Every SaaS product needs the same things before it does anything interesting: sign-in, billing, teams, API keys, webhooks, transactional email, a deploy pipeline. None of it is the product. All of it has to work on day one, because a broken password reset is a support ticket and a broken Stripe webhook is lost revenue.

So I built it once, as a template, and every product starts from a copy:

  • Auth: JWT sessions with Google OAuth and magic-link email. Three ways to sign in, one user model.
  • Billing: Stripe products, subscriptions, checkout and the customer portal, with webhook events synced back into Postgres.
  • Teams: multi-user accounts, email invites, roles, billing per account.
  • API keys: scoped, hashed with SHA-256, with expiry and usage tracking.
  • Webhooks and Zapier: both hang off the same Kafka event stream, so a product emits an event once and it reaches HTTP subscribers and Zapier alike.
  • Email: Resend for sending, with bounce, open and click tracking fed back in.
Product API Go Kafka event backbone Webhook delivery signed · retried · logged Zapier app same event stream emit
A product emits an event once; Kafka fans it out to webhooks and Zapier.

The backend is Go, the web app is React, and the design system is the same in every product: 60 components and 39 themes on one set of tokens, documented in a public Storybook. A new product starts with a rebrand script that asks for a name, a domain and a support address, then does a find-and-replace across the repo. After that, the work that’s left is the product itself.

One AWS account, not three

The second half is infrastructure. Three products could mean three of everything: three databases, three clusters, three load balancers, three bills that are mostly idle. Instead there’s a shared platform stack and a thin stack per product, both in Terraform.

The platform owns what’s expensive and generic: the VPC, one RDS Postgres instance, one ECS cluster, one Application Load Balancer, Kafka and IAM. It exports those as outputs.

Each product stack reads those outputs and adds only what’s specific to it: an S3 bucket and CloudFront distribution for the web app, a certificate, an ECS service, its secrets in SSM, and its DNS records.

Routing is the one clever bit. Each product’s CloudFront forwards /api/* to the shared load balancer with an X-Product-Id header. A listener rule per product matches that header and sends the request to that product’s ECS service. One load balancer serves every product, and adding a product means adding one rule.

shared platform · one AWS account sjocamp.co CloudFront + S3 meerkat.protoapp.xyz CloudFront + S3 orca.protoapp.xyz CloudFront + S3 Application Load Balancer one listener rule per X-Product-Id Sjocamp API ECS service Meerkat API ECS service TickUpToks API ECS service RDS Postgres one instance · a database per product /api/* + X-Product-Id
Each product owns its edge; everything behind the load balancer is shared.

Prototype or product is a single setting

Not everything I build deserves its own domain and alerting on day one. The product module has a tier setting:

prototypeproduct
Domain<name>.protoapp.xyzits own domain
Certificateshared wildcardits own
Log retention7 days90 days
Alarmsnoneunhealthy hosts, 5xx errors

Meerkat and TickUpToks run as prototypes on protoapp.xyz subdomains. Sjocamp runs as a product on its own domain, sjocamp.co. Which tier a product is on is a decision about how much it matters yet, not a different architecture.

The part nobody warns you about: local dev

Running three copies of the same stack on one laptop has its own trap. Each product’s local Postgres and Kafka need their own ports, so every product claims a numbered port slot.

I learned that one the hard way. Tests reach the local stack by port number. If one product’s stack is down and its ports collide with a sibling’s, its tests quietly connect to the other product’s database and Kafka, and still pass. Now a script assigns the slots, and claiming one is step two of starting any new product.

What sharing costs

It’s not free:

  • One database is a single point of failure. Each product has its own database on the shared instance, but an outage or a noisy neighbor affects everyone. For three small products this is an acceptable trade; for a product with real revenue it’s the first thing I’d split out.
  • Template drift is real. A fix to auth in one product has to flow back to the template and then out to the others. I keep the template as the upstream and port changes deliberately.
  • Coupled deploy knowledge. Changing the platform stack can affect every product at once, so platform changes are the ones I plan most carefully.

Why bother

As a solo builder, the scarce resource isn’t compute, it’s attention. Every hour spent rebuilding sign-in is an hour not spent on the thing only this product does. With a shared platform, a new product starts at “what’s the core idea” instead of “how do I set up Stripe again”, and that’s why there are three products and not one half-finished one.

If you’re building more than one product on your own, build the second one by extracting the first. The template you get from that is shaped by a real product, not by guesses.