Blog

Running Three Products on One PocketBase, Without Them Sharing Data

A single lightweight backend, three independent products, one operator. The engineering is mostly in deciding what stays isolated and what is safe to share.

product 1product 2product 3one PocketBase

Three of the products we run — a review platform, an anonymous posting site, and an AI tool — sit on the same server and, in one case, the same PocketBase instance. That is a deliberate cost decision: a client paying for one well-run server should not be paying for three lightly-used ones. It only holds up if a mistake in one product's data model can never leak into, or corrupt, another's.

Where the boundary actually lives

Not at the database engine — SQLite does not care how many logical products share a file. The boundary is enforced at the collection layer: access rules per collection, and no collection that spans two products' concerns. The anonymous platform's posts collection has no foreign key into the review platform's business records, because a foreign key is a promise that the two will always agree, and these two products have no business agreeing about anything.

The rule that is easy to get backwards

PocketBase's access rules read like the opposite of what most backends default to: an empty string means public, and `null` — no rule set — means deny everything. Assuming SQL-style defaults here fails closed in the annoying direction (nothing loads) rather than open (anyone can write), which is the safer way to be wrong, but it still costs debugging time if you have not internalised it.

listRule:   ""    // public
listRule:   null  // deny — this is the default, not "no restriction"

Denormalised fields need an owner, not just a schema

A rating aggregate on a business record looked, from the schema, like a solved problem — the field existed, the type was right. It had simply never been written to by anything, ever, because no code path updated it when a review was approved. The fix was a server-side hook on the reviews collection, firing on every create, update and delete path a review can take — including a direct edit through the admin UI, which a naive `on_create`-only hook would have missed entirely.

A field existing in the schema is not evidence that anything writes to it. That has to be checked separately, and it is worth checking early — before a dashboard has been quietly showing zero for months.

This came out of building UnmaskedWords Read the case study

Have something like this to fix?

Describe the problem and we will tell you what it takes.

Get in touchAll posts