Skip to content

Our product

Starterflare

An unreleased multi-tenant platform codebase covering workspaces, roles, profiles, a social graph, messaging, jobs and search.

Starterflare is a modular monolith in NestJS with a Next.js front end, built around multi-tenant workspaces with database-driven roles, professional profiles, connections, posts and articles, messaging, job postings and full-text search. The domain currently serves a login wall and nothing else: no public pages, no pricing, no billing code. It is on this page as an engineering sample, not as a product you can buy.

What it does

  • Multi-tenant workspaces with four database-driven roles
  • Email and password sign-in with verification, Google and Microsoft OAuth, and TOTP
  • Profiles covering experience, education, certifications, projects and skills
  • Connection requests, one-way follows and blocks
  • Short posts and long-form articles with reactions, nested comments and hashtags
  • Home, profile and hashtag feeds from a dedicated feed module
  • One-to-one messaging over WebSockets
  • Job postings with organisation pages, applications, saved jobs and alerts
  • Full-text search through OpenSearch, fed by an indexing outbox table
  • Moderation reports and an append-only audit log

Every so often a business asks for the internal version of a professional network: workspaces, profiles people keep up to date, a directory that is actually accurate, posts and articles, messaging, job postings and a search box that finds the right person. The data model for that sounds simple when it is described in a meeting and it is nothing of the sort. Starterflare is that model written out in full as a working codebase rather than a diagram, on a conventional server stack, and it has never been released. The domain serves a login wall and nothing else.

Why it exists

Most of what this studio runs sits on serverless infrastructure, and a preference held without a counter-example is just a habit. Some requirements point the other way: genuine joins across many tables, transactions that must hold, ranked full-text search with analysers and facets, durable events that can be replayed. Those are not things you approximate. Keeping a full-sized codebase built the traditional way means that when a client asks which model suits their problem, the answer comes with two sets of trade-offs we have actually lived with instead of one.

It is also the concrete version of an argument we make often. The interesting cost of a conventional backend is not what it costs at scale, it is what it costs at rest, with several stateful services that cannot be switched off without losing data or an index. We added that floor up properly in what a Postgres, Redis, broker and search stack costs at rest, including the line item nobody puts on the invoice, which is the person who upgrades Postgres and answers the phone when the broker wedges.

How it works

Ten domain modules in one deployable: identity, profile, social, content, feed, messaging, jobs, notification, search and admin. Underneath sit PostgreSQL 16 through Prisma, Redis for caching and rate limits, NATS JetStream for events, and OpenSearch for full-text search. Every identifier is a ULID rather than an auto-increment integer, so records can be created without a round trip and sorted by creation time without a separate column.

The part worth explaining is what happens after a user action. Consequences do not run inside the transaction that caused them.

accept a connection request
  transaction: write the connection row
               write a row into SearchOutbox
  publish connection.accepted -> JetStream
      -> notification module   writes a notification
      -> feed module           writes feed entries
  indexer: read outbox row -> push to OpenSearch -> mark done
           on failure, leave it and retry

A notification failing must never roll back the connection. A search cluster being briefly unreachable must never cost a user their action. The outbox table is the small, boring piece that makes the second guarantee true: the index write is recorded in the same transaction as the data change, then carried out separately and retried until it lands. It costs a table and a worker process, and it removes the worst class of bug there is, the user action that half succeeded.

Around that: profiles covering experience, education, certifications, projects and skills, each carrying a privacy level of public, tenant-only or connections-only; a social graph of connection requests, one-way follows and blocks; short posts and long-form articles with reactions, nested comments and hashtags, sanitised on the way in; one-to-one messaging over WebSockets; jobs with organisation pages, applications, saved jobs and alerts; and moderation reports backed by an append-only audit log. Operationally there is OpenTelemetry tracing, Prometheus metrics, Docker Compose for local work, Kubernetes manifests with autoscaling and ingress, and continuous integration that runs lint and tests against real Postgres 16 and Redis 7 containers rather than mocks.

The decision that shaped it

The first is the modular monolith, argued out in a decision record that sits in the repository next to the code. Services are a deployment answer to an organisational problem. A team that has not yet felt the organisational problem gets the operating cost with none of the benefit: distributed transactions, version skew, a tracing setup you need before you can debug anything at all, and a local environment nobody can run. Modules with enforced boundaries inside one deployable give you most of the discipline and let you split later along seams you have already drawn.

The bill for that choice is honest and worth stating. One deployment means one blast radius, so a memory leak in the search indexer takes the messaging gateway down with it. Scaling is all or nothing: the jobs module gets the same replicas as identity whether or not it needs them. And boundaries inside a single process hold only while somebody enforces them, because nothing physically prevents the feed module from reaching into the content module's tables.

The second is tenancy. Workspaces are rows in one Postgres database, with membership and four roles resolved at request time from the database instead of hard-coded: super admin, tenant admin, moderator, member. That is the standard design and it is the harder one to walk back, because isolation becomes something every query has to remember rather than something that is simply true. One missing filter in one endpoint is a cross-tenant leak. Backup and restore is per database, so returning a single tenant to yesterday means extracting their rows by hand. Objectify went the other way and gives each tenant its own small database, and the trade-offs of that split are set out in when many small databases beat one big one. Neither answer is free; the mistake is picking one without noticing you have picked.

Profile privacy is the third choice and the cheapest to get right. Three ordered levels rather than a page of independent switches, which is the same reasoning we applied on ClassProfile and set out in four visibility levels beat a page of toggles: a ladder of a few states can be tested and pictured, and a wall of toggles produces thousands of combinations that nobody has ever seen.

Where it stands today

Unreleased, and thinner than the schema makes it look.

The Prisma schema is by a distance the most finished artefact: 916 lines, roughly 45 models. The application source across backend, frontend and shared workspaces comes to about 6,500 lines, and the front end is 11 pages with two shared layout components. Picture a well-specified platform with a partial interface sitting on top of it, because that is what is there. A very large planning document in the root of the repository explains the imbalance: the system was specified in bulk up front, and the data model is what the specification produced most faithfully.

It has never been deployed. The Kubernetes manifests exist, the compose file exists, the continuous integration runs, and there is no delivery workflow and no running instance anywhere. So the floor cost we describe so confidently is a cost we specified and never paid, which is the ordinary failure it is fair to admit to.

Nothing charges anybody. There is no billing code, no checkout and no Stripe. A plan enum in the schema lists free, starter, professional and enterprise, and not one line of code reads it. The pricing, features, about and docs paths all return 404.

The awkward part is the domain itself. The live site offers passkey registration, passkey login and account recovery, and it is not obviously the same product as this repository, which contains no WebAuthn or passkey code at all and signs people in with an email address and password, Google or Microsoft OAuth, and a TOTP code. We are not going to pretend those are one thing. Whatever runs behind that login wall, and whether anyone uses it, we cannot verify. The repository has no commit history to check either, so we cannot tell you who worked on what or over what period. Its own planning documents talk about ten million users; the implementation does not support that description and we are not going to repeat it.

What this says about how we work

A portfolio that contains only trading products has been curated into uselessness. This entry is unfinished, and saying so is more use to a prospective client than a paragraph implying otherwise, because the same honesty is what you get in an estimate.

The practical version: before anyone writes code for a system like this, we price the floor. Six services that cannot idle, plus the fraction of a person who maintains them, is a number a business can look at in month one instead of month nine. Sometimes that number kills the design, which is the point of producing it. If you are weighing a platform build and want that arithmetic done for your case rather than ours, start a conversation and bring your traffic assumptions, however rough they are.

What we wrote about building it

More of the same kind

Want something like this?

Tell us the shape of the problem and we will tell you honestly whether we are the right people for it.

Start a conversation