Under the Hood

Security, privacy & observability: defense in depth, one question everywhere

Security isn't a feature you add; it's a property that has to hold on every path. This chapter pulls Fable's security threads together — the layered defenses (auth, rate limiting, idempotency, upload validation), the one authorization question asked identically everywhere ('is this user a member of this group?'), secrets handling with no credentials on disk, moderation and privacy for real users, and an observability posture that's deliberately lean. The through-line is defense in depth plus one consistent access rule, because the way you leak data is forgetting to ask that question on exactly one endpoint.

Security, privacy & observability: defense in depth, one question everywhere

Security is not a chapter's worth of features bolted on at the end — it's a property that has to hold on every request path, and it fails not with a dramatic breach but with a single endpoint that forgot to check something. Fable's security story is really two ideas: defense in depth (many independent layers, so one failing doesn't mean compromise) and one authorization question, asked identically everywhere (so there's no path where the check is subtly different or missing). This chapter gathers the threads the earlier chapters introduced and adds the two that didn't have a home: how secrets are handled, and how you know what the running system is doing.

The layers (defense in depth)

No single control is trusted to be sufficient; they stack:

The point of stacking them is that a gap in one is not a compromise: a leaked access token still expires and can be revoked; a bypassed rate limit still hits an authenticated, authorized, validated endpoint. Defense in depth means no single point of security failure.

The one question: "is this user a member of this group?"

Fable's entire authorization model reduces to a single question, and its power is its uniformity. Because everything hangs off a group — expenses, settlements, chat, media, stash, location — access control is always the same check: is the requesting user an active member of the group this resource belongs to? And crucially, that same question is asked on every access path, not a different way per feature:

  • REST reads/writes — the handler checks group membership (group_members) before returning or mutating a resource.
  • MediaassertReadable runs the identical membership check before handing back a receipt.
  • Realtime — a socket only joins a group's room if the user is a member, and room membership tracks group membership, so the live stream respects the same boundary.
  • Search — the denormalized search rows bake in member ids so the read-path ACL is enforced in the query — you can't match a result in a group you're not in.

One question, four surfaces, one answer. That uniformity is itself a security property, because it means there's exactly one rule to get right, not a per-feature patchwork where one surface drifts.

Secrets: nothing on disk to leak

The best-handled secret is one that doesn't exist as a file. Fable's database credential is the example: the API reaches Postgres through the cloud-sql-proxy, which authenticates using the VM's attached service account fetched from the cloud metadata server — so there is no database password or key file on disk anywhere in the container or the image. A leaked container filesystem yields no database credential because there isn't one to find; the identity is the machine's, granted by the platform, not a string someone wrote down. This is the keyless-auth pattern from the infra chapter, and it's the single highest-leverage secrets decision: eliminate the credential rather than protect it. Where secrets must exist (API keys, signing keys), they live in the environment/secret store, never in the repo — but the principle is always "can this secret not exist at all?" first.

Privacy & moderation: obligations, not features

Real users and app stores impose requirements that aren't optional:

  • Moderationreports write a row to a back-office review queue (rate-limited so they can't be used to spam the queue), and blocks are a mutual-hide (a blocked user's messages vanish from the blocker's view while they keep posting to shared groups). These are store-compliance mandates, shipped as requirements.
  • Privacy — media is access-controlled, not public-by-default; the EXIF-stripping gap is flagged precisely because a receipt photo's embedded GPS is a privacy leak, not a cosmetic one; and personal data (phone numbers, handles) is scoped to the groups you share.

Observability: deliberately lean, honestly

You can't secure or operate what you can't see, but observability is also where a solo project can over-invest. Fable's posture: error tracking via Sentry (exceptions, stack traces, and the context to debug a production failure) as the non-negotiable core, plus structured logs. What it doesn't run — a full metrics/APM/distributed-tracing stack — is a deliberate proportionality call: distributed tracing earns its keep across many services, and Fable is one process, so a stack trace from Sentry plus logs answers most "what broke" questions without a Prometheus/Grafana/OpenTelemetry install to operate. The honest framing: error tracking is correctness observability (you must know when the money code throws) and shipped; deep performance observability is an optimization that scales in when there's more system to observe — the same correctness-vs-optimization split as the media pipeline.

Planned (TDD)

Security as per-feature access logic; a fuller observability stack (metrics, tracing, dashboards).

Shipped

Defense-in-depth layers with a single reused group-membership authorization check across REST/media/realtime/search; keyless DB auth (no credential on disk); reports + blocks for compliance; Sentry error tracking + structured logs.

One uniform authorization question is far harder to forget than per-endpoint checks — the way multi-tenant apps leak is a single endpoint that omits the check. And observability is scaled to a single process: error tracking is non-negotiable, deep tracing is deferred until there's a distributed system to trace.

Interview takeaway

  • "Authentication is not authorization." The highest-signal security sentence: a logged-in user isn't entitled to every row, and the classic leak (IDOR) is an authenticated endpoint that skipped the ownership check.
  • "I make authorization one reused question applied on every access path — REST, media, realtime, search." Shows you defend against the forgotten check, not just the written one.
  • "Defense in depth: TLS, auth with revocation, rate limiting, idempotency, input validation — no single control is load-bearing."
  • "Eliminate secrets rather than protect them — keyless workload identity means no DB credential on disk to leak."
  • "Observability scaled to the system: error tracking always, distributed tracing when there's a distributed system."

Security threads through every chapter — auth, media validation, rate limiting, idempotency — because that's what defense in depth means: it isn't anywhere, because it's everywhere.

Go deeper

  • OWASP Top 10 Broken access control (IDOR) sits at #1 — the vulnerability this chapter's "one question everywhere" is built to prevent, with the full catalog of what else to defend.
  • Google — workload identity & keyless credentials The pattern behind "no credential on disk": a workload authenticating as its attached identity rather than a key file — the highest-leverage secrets decision, documented.
  • Sentry for NestJS The error-tracking layer Fable actually runs, and a sense of what "correctness observability" gives you before you invest in a full metrics/tracing stack.

Check yourself

Answer out loud, as if an interviewer asked. If you hand-wave, reread that section.

  1. Explain "defense in depth" using Fable's layers, and why a gap in one layer (say a leaked access token) is not a compromise.
  2. Fable reduces authorization to one question. State it, name the four access surfaces it is asked on, and explain why that uniformity is itself a security property.
  3. What is an IDOR / broken access control, and why does "authentication is not authorization" capture the mistake? How does a single reused authorization check defend against the way it actually happens?
  4. Explain "eliminate the secret rather than protect it" using the keyless cloud-sql-proxy. Why is having no credential on disk stronger than having a well-protected one?
  5. Why does Fable run Sentry error tracking but not a full metrics/tracing stack? Frame the choice as correctness-vs-optimization observability and tie it to the single-process architecture.
  6. Give two ways Fable handles privacy/moderation obligations (not features), and explain why EXIF stripping is classified as a privacy issue rather than a cosmetic one.

Infrastructure evolution: when ₹0/month started costing 70 milliseconds

Fable launched on an all-free-tier stack — Neon Postgres and Upstash Redis in Singapore, a Mumbai VM for the API, Cloudflare R2 for media — because validating the product mattered more than owning the infrastructure. Then every screen felt slow, and the reason was geography: the API in Mumbai paid a cross-region round trip to the database in Singapore several times per request, on top of serverless cold starts. This chapter is the honest migration story — why free tiers were the right first call, the options weighed, the move to managed Cloud SQL in Mumbai with self-hosted Redis on the VM, and the real gotchas that turned a one-afternoon migration into four commits: a Postgres major-version surprise, unused PostGIS extensions that broke the restore, and a health check that wouldn't let the API start.

Scaling story & retrospective: the seams were the real design

The closing chapter, and the honest one. Fable runs on a single VM with a single Postgres, no replicas, no shards, no queue — and that's not a confession, it's the design working. This chapter lays out the scaling ladder in the exact order Fable would climb it, showing that the real engineering wasn't building distributed infrastructure but leaving the seams where it could be added: ULIDs and group-scoping for sharding, a wired-but-idle Redis adapter for realtime, idempotency everywhere for at-least-once. Then a frank retrospective — what the constraints got right, what broke in production, and the one lesson the whole series is really about.