Skip to content
← Projects Academic Project

FakeStore BaaS

Multi-tenant headless e-commerce Backend-as-a-Service with complete data isolation via PostgreSQL's native Row-Level Security. Stripe billing, BullMQ webhook queue with exponential retry, national shipping, Cloudflare R2 uploads, and 215 automated tests across 28 suites. Academic project built with production-grade architecture.

01

Context

The goal was to build a headless e-commerce API that any merchant could integrate via API key — without sharing a database, without risk of data leakage between tenants. The central challenge was implementing multi-tenant isolation structurally, not by code convention: a query missing a WHERE clause should not return another tenant's data even in the event of a bug.

02

Role & Team

Role Backend Developer (solo)
Timeline Ongoing development
Team Solo
03

Options considered

WHERE-clause isolation in application code Rejected

Simple, but any bug or forgotten query leaks data across tenants — security depends on convention, not mechanism

Native PostgreSQL Row-Level Security (RLS) Chosen

SET LOCAL app.current_tenant_id per transaction — the database itself refuses any out-of-tenant access. Zero leakage even with bugs in Node.js code

Prisma ORM Rejected

Good query ergonomics, but the abstraction layer prevents the fine-grained SET LOCAL control needed to activate the RLS context per transaction

Knex.js query builder Chosen

Explicit queries, raw SQL support for SET LOCAL, granular transaction control — a perfect fit for the RLS model

Inline webhook delivery on request Rejected

Simple, but a slow client endpoint blocks the process with no retry — a failed webhook leaves the client with no notification

BullMQ worker in an isolated process Chosen

Separate worker, exponential retry (5×), HMAC-SHA256 signatures, delivery audit trail — real product behaviour

04

Solution

Architecture

Express 4 + TypeScript multi-tenant BaaS. Each request resolves the tenant via SHA-256 of the x-api-key (Redis cache 5 min). RLS-scoped transactions with SET LOCAL — the database isolates data structurally. A separate BullMQ worker handles async webhook delivery.

Technical highlights

  • RLS via SET LOCAL app.current_tenant_id — zero cross-tenant data leakage even with a bug in application code
  • 3-layer authentication: API key (SHA-256 hash), JWT RS256 (user), API Secret (bcrypt S2S)
  • Webhook engine isolated in its own Docker container — BullMQ + exponential retry + HMAC-SHA256 signatures
  • Monetary values stored as integers (cents) — floating-point precision bugs eliminated by design
  • TypeScript SDK auto-generated from the OpenAPI spec (openapi-generator-cli)
  • 215 tests in 28 suites: 13 integration (including RLS isolation) + 15 unit

API / Backend: Express 4 · PostgreSQL 16 + RLS · Redis 7 + BullMQ · Stripe · Cloudflare R2 · Melhor Envio

05

Results

Metric Value
Automated tests 215 tests / 28 suites
Integration suites 13 (includes RLS isolation)
Unit suites 15 (middlewares, services, utils)
CI/CD GitHub Actions (push + PR to main)
Multi-tenant isolation Native RLS — DB refuses cross-tenant access
06

Learnings

What it is and what it isn’t

FakeStore BaaS has no frontend. It’s a headless API — merchants integrate via API key and build any interface they want on top. The “Fake” in the name isn’t pejorative: it’s a reference to the public FakeStore API used in tutorials, which this project replaces with a real, multi-tenant, production-ready version.

Why RLS and not WHERE clauses

The most important decision in the project was where to implement data isolation. The conventional approach is to add WHERE tenant_id = $1 to every application query. It works — until someone forgets, or a JOIN bypasses the filter, or a debug query runs in production without the tenant context.

With RLS, PostgreSQL refuses any access to rows outside the active tenant in the transaction — regardless of what the Node.js code does. The mechanism:

SET LOCAL app.current_tenant_id = '42';
-- From this point on, any SELECT/INSERT/UPDATE/DELETE
-- automatically filters tenant_id = 42
-- Rollback or end of transaction clears the context

The RLS integration test suite verifies this explicitly: it creates two tenants with separate data and confirms that queries from one return nothing from the other — even with raw SQL.

Webhook worker in a separate process

The webhook engine runs in a completely separate Docker container from the API. When an order is confirmed, the API pushes a job to the BullMQ queue. The worker consumes asynchronously, signs the payload with HMAC-SHA256, and delivers with exponential retry (up to 5 attempts, base 60s). Every delivery — success or failure — is recorded in webhook_events.

This means a client endpoint that takes 10 seconds to respond cannot block any API request. And the client can audit exactly when each webhook was delivered and with what HTTP response.

Money as Integer

All monetary values in the database are cents (Integer). R$ 49.90 becomes 4990. Percentage discounts are basis points: 15% = 1500. Conversion to reais only happens in the JSON response. This eliminates 0.1 + 0.2 = 0.30000000000000004 structurally — no developer needs to remember to use toFixed(2).

Source code ↗