Braandly
Multi-tenant brand management SaaS, and the API surface built on top of it.
- Data models
- 85
- MCP tools
- 102
- Unit tests
- 699
- Since
- 2021
The problem
Brand assets live everywhere except where anyone can find them. The logo is in a Drive folder, the palette is in someone’s Figma file, the guidelines are a PDF two versions out of date, and the person who knows which is current left in March.
Agencies have the same problem multiplied by every client they run, and the tools that solve one slice of it do not talk to the tools that solve the next.
What I built
A workspace-scoped platform where a brand and everything attached to it — identity, guidelines, palettes, fonts, gradients, files — sits in one place, with projects, tasks and collaborative documents around it, plus public link-in-bio pages for brands that need an outward face.
It is a Next.js front end against an Express and MongoDB API, with Redis behind the queues and caches, and Socket.IO plus Yjs carrying real-time document collaboration. The front end deploys to Vercel; the API runs as a Docker image on a VPS, built and shipped by GitHub Actions.
Decisions I'd defend
Tenancy is shared-schema: one database per environment, and a workspace reference on every domain collection. Every compound index is workspace-prefixed, including the full-text ones, because a text index that is not tenant-prefixed will happily search across tenants.
Scoping is enforced in three layers rather than one. Route middleware resolves membership and role from the canonical workspace document rather than the denormalised copy on the user. A generic guard then checks the requested resource actually belongs to that workspace, and returns 404 rather than 403 so the API never confirms that another tenant’s record exists. Controllers still filter explicitly on top of both.
Authorisation has four levels, not one: a platform role, a workspace role ranked so nobody can act on someone above them, a per-project permission matrix stored on the project, and per-document sharing with view, comment and edit tiers.
The public API needed a real auth story, so I built an OAuth 2.1 authorization server: authorization-code only, PKCE mandatory, dynamic client registration, RS256 access tokens verified statelessly, and opaque refresh tokens hashed at rest and rotated on every use, with reuse detection that revokes the whole token family. API keys are hashed too, with only a masked prefix stored for display.
On top of that sits an MCP server exposing 102 tools, stateless over streamable HTTP with a fresh instance per request, its types generated from the live OpenAPI spec so the tools cannot drift from the API they wrap.
What went wrong
A live API audit found that workspace roles were not being enforced on most write endpoints — a guest could create tasks, projects and documents, and update brands. That is a privilege escalation, and it existed because scoping and authorisation had been treated as the same problem when they are not: the middleware correctly established which workspace you were in, and then did not check what your role there allowed you to do.
The fix was a dedicated write-authorisation layer applied across the mutating routes, plus a hardening pass on the invitation flow. Worth stating plainly rather than quietly patching, because the interesting part of a permissions model is where it failed.
Where it is now
Live and in active development since late 2021, across four repositories. 85 data models, 699 unit tests, a Figma plugin and a browser extension against the public API, and a Playwright sweep that walks every route checking for console errors, failed requests and accessibility violations.
I am the sole author of the backend, the link-in-bio app and the MCP server, and I wrote the large majority of the front end, with two collaborators contributing in bounded windows.