Everything starts on one server
Designing for millions of users is hard — so you never start there. You start with a setup that serves a single user, understand every piece, then add complexity only when something forces you to. In the simplest setup, one machine runs everything: the web app, the database, and the cache.
The user's browser resolves your domain through DNS, sends an HTTP request, and the one server returns a response. Simple to reason about — and fragile, because there is nothing behind it.
Where the data lives
Storage selection comes down to two families. Pick by the shape of your data and the access pattern, not by hype.
Relational databases (RDBMS) structure data into tables — like spreadsheets. Columns are the fields/attributes; rows are individual records. They use SQL to query and manipulate that data, and they shine when relationships and consistency matter.
Best for
Structured data, clear relationships, transactions that must stay consistent (payments, orders, accounts).
Examples
PostgreSQL MySQL Oracle SQLite
-- a customers table: columns = fields, rows = records SELECT id, name, email FROM customers WHERE id = 123; -- 123 | John | john@mail.com
NoSQL databases trade rigid schemas for flexibility and fast access to large volumes of unstructured data. There are four common types — each solves a different problem.
📄Document
Flexible JSON-like docs. MongoDB.
🪵Wide-column
Massive scale, write-heavy. Cassandra.
🕸Graph
Entities + relationships. Neo4j, Neptune.
⚡Key-value
In-RAM, blazing fast. Redis, Memcached.
Two ways to handle more traffic
When one server can't keep up, you scale. There are exactly two directions — and the difference between them decides your reliability story.
Add power to one server
Bolt on more RAM, CPU or other resources to your existing machine. Dead simple, and fine for low-to-moderate traffic.
- Resource limits — there's a hard cap on how big one box can get. You will hit the ceiling.
- No redundancy — if that one server dies, the whole app goes down with it.
Add more servers
Spread the load across many machines that share the work. This is what high-traffic systems do — it needs a load balancer in front, but it buys you redundancy and near-unlimited room to grow.
- Redundant — one server failing doesn't take the app down.
- Elastic — add a 4th, 5th, Nth server whenever you need.
The traffic cop: load balancing
The box sitting between clients and your servers is the load balancer. It distributes incoming requests across your pool, sending each to whichever server has the least load. It does three jobs at once:
⚖️ Distribute
Spreads traffic evenly so no single server gets overwhelmed.
🛟 Fault tolerance
If a server goes down, it stops routing there and shifts traffic to the rest.
📈 Scalability
Add a new server and the LB folds it into rotation automatically.
How does it pick a server?
Click an algorithm to see how it routes:
Health checks — how the LB knows a server died
The load balancer constantly pings each server with a health-check request. The moment a server stops answering, the LB marks it offline and routes nothing to it — until a later health check succeeds and it's back in rotation. No human in the loop.
The single point of failure
A single point of failure (SPOF) is any one component whose failure brings the entire system down. Classic case: many API servers, but all of them talk to one database.
If that database dies, none of the APIs can read or write, and every client gets nothing back. SPOFs are dangerous because they concentrate risk — the fix is redundancy: replicate the database, run multiple load balancers, and remove any box that the whole system leans on alone.
An API is a contract
API stands for Application Programming Interface — it defines how software components talk to each other. On one side a client (browser, mobile app); on the other a server that responds. The API is the agreement between them:
- What requests can be made — which endpoints exist
- What methods are allowed on each endpoint
- What the request and response should look like
/api/admin/business). And your WAT tools/*.py scripts are your internal API — the workflows/ markdown files are their contracts. Same idea as a web API, different runtime.Protocols & the transport layer
Application-layer protocols sit at the top of the network stack, built on transport-layer protocols below them. When building APIs you mostly care about the application layer — but it helps to know what's underneath.
🚚 TCP reliable
Connection-oriented. Guarantees ordered, complete delivery. The default for APIs and the web.
🏎 UDP fast
Connectionless, no delivery guarantees — but low latency. Good for streaming, gaming, real-time.
Application-layer protocols
HTTP / HTTPS
The foundation of web APIs. Client sends a request (method, headers, body); server returns a response (status code, content-type, body). HTTPS = HTTP + TLS/SSL encryption.
WebSockets
Persistent, bidirectional connection for real-time data — chat, live video, collaborative apps.
gRPC
Google's RPC protocol. Fast and compact — popular for service-to-service calls in microservices.
AMQP
Advanced Message Queuing Protocol — for asynchronous, queue-based communication.
The HTTP request/response cycle
// REQUEST GET /products HTTP/1.1 Host: api.shop.com Authorization: Bearer <token> // RESPONSE HTTP/1.1 200 OK // 2xx ok · 4xx client error · 5xx server error Content-Type: application/json Cache-Control: max-age=60
API styles: REST, GraphQL, gRPC
The most common style for web and mobile. REST is stateless — every request carries everything needed to process it, so no prior request is required. It maps cleanly onto standard HTTP methods, and HTTP status codes pair beautifully with CRUD operations.
Click a method to explore
GraphQL is a query language — the second most common style after REST. It exposes a single endpoint and lets the client ask for exactly the fields it needs, no more, no less. It runs over HTTP. Operations are queries (read) and mutations (write).
query { product(id: 301) { title # ask for only what you need price } }
Solves REST's over-fetching / under-fetching problem — but adds query-complexity and caching challenges.
gRPC uses compact binary messages over HTTP/2, which makes it much faster than JSON-over-HTTP. It's the go-to for microservice-to-microservice calls inside your architecture, where speed matters more than human-readability.
✅ Fast
Binary payloads, multiplexed streams.
✅ Typed
Strict contracts via protobuf.
⚠️ Less human-readable
Harder to debug by eye than REST.
Protocol choice shapes the API
The protocol you pick affects structure, performance and capabilities — so match it to the job:
- REST → HTTP + status codes, ideal for CRUD over the web
- Real-time APIs → WebSockets for chat / streaming
- Microservices → gRPC for low-latency internal calls
Designing APIs that hold up
Two pillars the course stresses for production-grade APIs:
🔒 Security
- Authenticate & authorize every caller
- Validate all inputs
- Apply rate limiting
⚡ Performance
- Cache where it helps
- Paginate large lists with
limit&offset— never return thousands of rows at once - Minimize payloads & reduce round trips
// pagination keeps responses small and fast GET /products?page=2&limit=3 // → { page: 2, limit: 3, total: 10, products: [...] }
Authentication — who are you?
Authentication answers one question: who is the user trying to access the system? Before anyone reaches your API gateway, services, or data, they must prove their identity via a login request. The course untangles a lot of commonly-confused terms:
MYTH "JWT is an authentication method" +
MYTH "Bearer auth and JWT are the same thing" +
MYTH "OAuth 2 is an authentication method" +
MYTH "SSO is an authentication method" +
The methods, lightest to heaviest
Basic
Username + password in the header. Simple, weak alone.
API keys
A static secret identifying a caller/app.
Sessions + cookies
Server stores session state; cookie holds the id.
Bearer / JWT
A signed token sent on each request.
Access & refresh tokens
Modern systems issue two tokens at login:
short-lived · 15 min – 1 h
Used for actual API calls. Short lifetime limits the damage if it leaks.
long-lived · days – weeks
Used only to mint new access tokens when they expire. Store it in an HTTP-only cookie — never in local storage, so client-side scripts can't steal it.
Why JWTs scale: stateless verification
Because the token is signed, the API server validates it with the signature alone — no round-trip to a session store. That statelessness is exactly what lets you add servers freely behind a load balancer.
Higher up the stack: OAuth 2 (delegated authorization), OpenID Connect (identity on top of OAuth), and SSO (one login, many apps).
createServerClient + getUser, cookie-backed, JWT under the hood) — exactly the stateless story above. Your dashboards use a lighter gate: a client-side passphrase (AES-GCM, KB_PASSPHRASE) for the encrypted ones, or edge basic-auth for the private decks and the site's beta gate.Authorization — what can you do?
Once we know who you are, authorization decides what you're allowed to touch. The workhorse pattern is Role-Based Access Control (RBAC): users get roles, and each role carries a fixed set of permissions.
| Role | Read | Create / Update | Delete | Manage users |
|---|---|---|---|---|
| Admin | ✅ | ✅ | ✅ | ✅ |
| Editor | ✅ | ✅ | ❌ | ❌ |
| Viewer | ✅ | ❌ | ❌ | ❌ |
is_admin()). The admin route gates with 401/403, and writes use a service-role client that bypasses RLS — powerful, so that key must stay server-side only. (The code itself notes the admin check is still "simple".)Securing the API surface
Beyond auth, four habits keep an API from getting breached:
🔐 HTTPS everywhere
TLS/SSL encrypts traffic in transit so nothing travels in the clear.
🌍 CORS
Cross-Origin Resource Sharing controls which domains may call your API from a browser. Without it, a malicious site could ride a user's browser to hit your API. Allow only your own front end.
💉 Injection prevention
SQL / NoSQL injection happens when user input is dropped straight into a query. Validate & parameterize everything.
🚦 Rate limiting
Cap requests per client to blunt abuse, scraping and brute-force attacks.
Now point it at your stack
The same lenses, turned on what you actually run — grounded in your repo, not generic advice. Reassuring news first: the architecture is sound. The flags below are mostly "confirm" and "back this up", not "rebuild".
Thalamus app
- ✓ Relational Postgres fits the data — goals, votes, providers and profiles are all relationships.
- ✓ Stateless Supabase auth (cookie / JWT) — scales cleanly behind the edge.
- ✓ RLS hardening (migration 008) closed two "world-readable" holes; the app already reads the safe
verification_publicview. - ⚠ Confirm migrations 004–008 are actually applied in the live Supabase — the app depends on that view (if a provider page 500s, they're not applied).
- ⚠ The admin check is "simple" (its own code comment) — formalize roles before adding more admins.
- ⚠ The service-role key bypasses RLS — must stay server-side only, never
NEXT_PUBLIC_.
ilonagolman.com
- ✓ Tiny attack surface — mostly static pages on a CDN.
- ✓ One env var flips public ↔ private (
PREVIEW_PASSWORD) — no code change to launch. - ⚠ That beta password is the only wall; opening to the world = remove it + redeploy.
- ⚠ DNS still pending — the domain 404s; the stable link is the Vercel alias for now.
Command Center + dashboards
- ✓ No server, no DB → almost nothing to attack.
- ✓ Encrypted ones decrypt client-side with
KB_PASSPHRASE— the data never sits in plaintext on the host. - ⚠ Client-side crypto means the ciphertext is public — security = passphrase strength + never leaking it.
- ⚠ Data lives as repo JSON (version-controlled) — keep PII like lead names out of it, as you already do.
WAT tooling + secrets
- ✓ Deterministic scripts, version-controlled, easy to re-run — the WAT split is doing its job.
- ⚠
.envis your single secrets SPOF (Supabase, Vercel, Cloudflare, Stitch, GitHub tokens) — no backup, no redundancy. - ⚠ Rotate
STITCH_API_KEYif it was ever shared or exposed.
What to check first
- Confirm Supabase migrations 004–008 are live (the app reads
verification_public— a 500 on a provider page means they're not applied). - Verify no service-role / secret key is exposed as
NEXT_PUBLIC_anywhere in the frontend. - Rotate
STITCH_API_KEYif it ever left your machine. - Make a secure backup of
.env— it's your single secrets point of failure. - Decide the beta-gate → public flip (DNS + remove
PREVIEW_PASSWORD) when you're ready to launch.
Why this is the skill to learn now
AI writes most of the implementation now, so interviews have shifted. The system-design round tests something AI can't fake for you: whether you understand how components fit together at a high level, can make architectural decisions at scale, and can articulate the trade-offs of what you'd build.
Want the full 2-hour walkthrough with every diagram?
▶ Watch on YouTube — Hayk Simonyan