Back to all writing

I Built the Backend Before I Had Anyone to Trust It With

IPMAT Series•Part 33•7 min read•By Mikhil

I Built the Backend Before I Had Anyone to Trust It With

There is nobody depending on this product at scale yet.

That is exactly why this is the right time to make the dangerous mistakes.

Privately.

With my own account.

Before somebody else loses progress because I decided reliability could wait until launch week.

The shared backend work changed the tone of the project.

For a long time, I was building features.

Now I was building rules.

Rules about what the browser is allowed to claim.

Rules about what the server verifies.

Rules about what can change.

Rules about what must never be counted twice.

Rules about who owns what.

That felt much closer to building infrastructure than building another app screen.

The browser stopped being trustworthy by default

This sounds harsh.

The browser is not evil.

It is just not authoritative.

A client can be stale.

Offline.

Modified.

Buggy.

Out of order.

Running old code.

So important things should not become official merely because the browser says they happened.

Grading is the obvious example.

The server should know the canonical answer.

The client submits a response.

The server grades it.

Not:

The browser sends:

correct = true

and the database politely believes it.

That becomes more important the moment progress, limits, achievements or access depend on correctness.

Answer keys should stay private

This was another basic rule.

A public client needs the question.

It does not necessarily need the answer key before submission.

If every canonical answer is sitting in an easily readable payload, exam integrity becomes theatre.

So answer keys belong behind the server boundary.

Again:

Not because the learner is the enemy.

Because the product should not make bypassing its own rules trivial.

Content versions became immutable

A published question is not just text.

It is a piece of historical evidence.

If the wording changes tomorrow, yesterday's attempt should still refer to yesterday's version.

That means the backend needs immutable versions and mutable publication pointers.

Editors can publish a better version.

The old attempt does not get rewritten.

This principle applies to questions.

Vocabulary.

Mocks.

Anything that contributes to learner history.

The past should remain explainable.

XP taught me about replay protection

XP sounds harmless.

It is also a great way to expose duplicate-event bugs.

Suppose the same successful response retries three times.

Should the learner receive XP three times?

Obviously not.

So rewards have to attach to stable evidence.

Once per relevant event or content version.

Not once per request.

That is the same idempotency problem from sync showing up in gamification.

Infrastructure leaks into everything.

Offline needed bounded trust

I still want authenticated learning to work through temporary network loss.

That creates another trade-off.

The app cannot ask the server for permission before every single interaction if the device is offline.

It also cannot be allowed to invent unlimited server-approved history forever.

So the backend architecture includes a bounded offline model.

Enough room for legitimate offline work.

Enough structure that the server can reconcile it later.

That is much more complicated than:

Offline: yes/no.

Device isolation became part of security

A learner can have multiple devices.

A device can be revoked.

A device can hold pending work.

A device can be old.

The backend needs to know enough about those relationships to prevent stale or revoked clients from behaving like permanent trusted agents.

This is another thing I would never have thought about when the app was local-only.

The database needed least privilege

Early projects often use permissions that are too broad because it is convenient.

The client needs data.

So give it table access.

Done.

That becomes risky very quickly.

The browser should only be able to do what the product genuinely requires.

Sensitive writes should go through narrower paths.

Derived Intelligence state should not be casually editable by the learner client.

Billing records should definitely not be.

Editorial publishing should definitely not be.

The backend should make invalid states difficult to create.

Not merely hope the UI never creates them.

Intelligence needed stronger authority than normal UI state

The learner can create evidence through learning actions.

The learner should not directly rewrite the official inference derived from that evidence.

Otherwise the model stops being a model.

So the architecture separates:

Learner-owned factual events.

Server-derived state.

Versioned snapshots.

Recommendations.

Intervention outcomes.

That gives Intelligence a cleaner trust boundary.

The app can explain the result.

The server owns the official derivation.

Billing made the same principles even stricter

The billing architecture is not the interesting part of this article, and it is not fully operational yet.

But designing for it exposed familiar problems.

Duplicate provider events.

Out-of-order events.

Refunds.

Retries.

Reconciliation.

Trials.

One event arriving twice should not create two purchases.

A late event should not resurrect an old entitlement accidentally.

Again:

Stable identity.

Ordering.

Idempotency.

Server authority.

The same boring ideas keep winning.

Security verification needed its own script

The backend package includes explicit verification rather than assuming migrations imply safety.

That matters because:

I wrote an RLS policy

is not the same as:

The deployed roles cannot access what they should not access.

The verification step is another independent gate.

And even that does not replace staging with real authenticated accounts.

At some point the system has to be attacked like a user would attack it.

The backend is still a migration candidate

This is the most important caveat.

I am not writing this from the position of:

Production backend complete.

The shared backend is a serious tested migration candidate.

It still needs the real sequence:

Read-only inventory.

Backup.

Staging clone.

Apply.

Security verification.

Legacy import.

Client integration.

Hosted testing.

Two real accounts.

Multiple devices.

Cutover rehearsal.

Only then production.

The fact that SQL exists is not the same as the system being deployed safely.

That restraint is part of the architecture

I actually like that the migration package refuses some unsafe conditions.

Unexpected schema?

Stop.

Existing platform installation?

Stop.

Failed transaction?

Rollback.

That is better than "best effort."

A migration touching the foundation of three products should be boring and defensive.

Why build this before launch?

Because launch is the worst time to discover that identity is fragile.

Or that duplicates inflate progress.

Or that answer keys leak.

Or that account switching mixes queues.

Or that historical questions mutate.

Those are not polish problems.

They damage trust.

And trust is much harder to repair after real users depend on the product.

I still have the luxury of being early

The apps are private enough that I can break them.

The database can still be redesigned carefully.

The client contracts can still change.

The product can still admit:

This is not ready.

That is an advantage.

I do not want to waste it by rushing into public use with architecture I already know is inconsistent.

The backend became a promise

Not a technical promise.

A user promise.

Your answer will be graded against the right version.

Your progress belongs to you.

A retry will not duplicate it.

Another account will not inherit it.

Your history will still make sense later.

The product will not casually rewrite what happened.

That is what the backend is really for.

The SQL is just how the promise gets enforced.