Membership PlatformsField engagement

The brief was a front-end rebuild. The member table let a user set their own plan, so the first deliverable was the lock rather than the door.

Architecture and hardening lead.
The approach, in four words
check the lock firstthe column list mattersassume exposure until checkedinstalls have their own storage
Can a member change their own plan tier?NOYESnow rebuildclose this firstWith escalation closed andanonymous-key exposurechecked, the visible workproceeds on a record whoseA policy permitting a user toupdate their own row iscorrect for a display name andcatastrophic for a paid tier.
A redesign inherits every access defect the old interface had, and the inheritance is invisible precisely because the interface changed.
What was at stake

A live paid membership product wanted a rebranded front end, the ability to install it on a phone's home screen, and eventually a submission to an app store. The front end is what members see. Underneath it sits the member record, which decides who has paid for what. A rebuild that sits on that record inherits every problem the record has, and a fresh interface makes those problems harder to spot, because everything looks newly made.

How this works

When the browser talks to the database directly, the access policy is the whole security model.

Many modern applications have no server of their own between the user and the database. The front end talks to a hosted database directly, through an interface the platform generates automatically from the tables. To make that work, the application ships with a public key embedded in its code. Anyone who opens the application can read that key, so it identifies the application and grants nothing private. Whatever that anonymous key can reach, anyone on the internet can reach, with a few lines of script and no need for the application at all.

Security in that arrangement comes almost entirely from row-level policies in the database. A policy is a condition attached to a table. For reading, it decides which rows a request can see, and for writing, which rows it can change. A typical rule says that a member may update their own row. That rule is about rows. It says nothing about which fields within the row may change, so unless column privileges are restricted separately, a member allowed to edit their row can edit every field in it, including the one that records what they paid for. Nothing in the interface prevents this, because the request never needs to come from the interface.

Two further details catch teams out. What a table exposes by default is the platform's choice, and platforms have changed those defaults over time, so a table created under one default can behave differently from one created under another. And a web application installed to a phone's home screen may run in a separate storage container from the browser it came from, so a login made in the browser is simply absent when the installed app opens. Both are platform behaviours that change between versions, and both have to be checked against the platform as it currently stands.

  1. App holds a public key
    readable by anyone who opens the app
  2. Request goes straight to the database
    through an interface generated from the tables
  3. Role set from key and session
    anonymous, or a signed-in member
  4. Row policy checks which rows
    a member may change their own row
  5. Column privileges check which fields
    if absent, every field in the row is writable
  6. Change is written
    the interface was never consulted
Hiding a field in the interface does nothing. The request can be sent without the interface.
FigureWhat happens when a member's app writes to the database. There is no server in the middle to apply business rules. Row policies decide which rows can change, and only column privileges decide which fields, so a missing column restriction is the whole difference between editing a name and granting a paid plan.
The constraint

The value and the risk both sit at the seam between the new front end and the record beneath it. Here that seam held two problems. A self-escalation path in the access policy let a member grant themselves a paid tier. A default that exposes tables to the anonymous key meant the record was readable far more widely than intended. Neither is visible from the interface, and a rebuild improves neither.

Where it usually breaks

Access defects survive redesigns, because nobody looks below the interface.

In applications where the browser talks to the database directly, the interface is not a security boundary. A button hidden from members does not stop a member from sending the request the button would have sent. Defects in the access policy are therefore invisible in the product itself, and they can only be found by making requests the interface never makes, from a member session and from the anonymous key.

A rebuild compounds this. Attention moves to the new interface, testing focuses on whether the new screens work, and the record underneath is treated as settled because it has been running for a while. Running for a while proves only that nobody has tried, or that nobody who tried has mentioned it.

01

The editable plan column

A member may update their own row, the plan tier is a column in that row, and nothing restricts which columns change. A member upgrades themselves for free.

02

The anonymous key that reads everything

Tables are exposed to the public key by default, and policies were never added to some of them. Anyone with the key can read them.

03

The default that changed underneath

The platform changes what new tables expose on a dated notice. Tables created before and after the date behave differently, and the team assumes they are alike.

04

The logout nobody coded

Members install the app to their home screen and find themselves signed out, because the installed app has its own storage separate from the browser.

05

The redesign that hides the defect

A new interface ships on the old record. Every access defect comes along, now harder to notice because everything looks freshly built.

Member edits their display name
Request to update own row
sent from a member session
Policy: own row allowed
the row belongs to this member
Name changes
correct behaviour
Member edits their plan tier
Request to update own row
same session, different field
Policy: own row allowed
the same rule, satisfied the same way
Paid tier granted for free
self-escalation
Same rule, different column
The fix is about columns. Rewriting the row rule would not have changed this.
FigureOne policy, two columns. The rule that lets a member update their own row is exactly right for a display name. Applied to a row that also holds the plan tier, the same rule lets a member grant themselves a paid plan, and no error is raised anywhere.
The fork

The reflex, and the fix.

Road not taken

Rebuild the front end as asked

Pull

It is the agreed scope, it is visible, and the client can judge progress every week.

Why not

It puts a new interface on a record whose access rules nobody has checked. A design project becomes a design project with an unexamined access boundary inside it.

Road taken

Check member access first, then rebuild

Accepted

Splitting the first milestone so the access check comes before any visible work, which is harder to sell and a shorter path to a product the client can defend.

Bought

A rebuild standing on a verified record, with the escalation path closed before anything is built on top of it.

Decision

Check the lock before repainting the door. New paint does not change who can get in.

How it was built
01Access check
›
02Close escalation
›
03Rebuild
›
04Install and notify
›
05Submit
01

Verify who can change what

A policy that lets a member update their own row is correct for a display name and disastrous for a plan tier. Which columns the rule covers matters as much as the rule itself.

02

Check what the anonymous key reaches

Platform defaults have historically exposed tables broadly, and a published scan found a large number of databases readable in the wild. The working assumption is exposure until a check shows otherwise.

03

Know which side of a platform change you are on

A dated platform change to automatic table exposure alters the default posture. The correct configuration before that date and after it are different, so a rebuild that spans the date has to know which side it is on. The date and the new defaults have to be re-verified against the platform's own notice when the work is done.

04

Plan for installed-app storage

On one major mobile platform, a web application installed to the home screen does not share storage with the browser. Sessions do not carry across, and members appear to be logged out for reasons nothing in the code explains.

05

Then rebuild

With the record verified and the escalation path closed, the visible work goes ahead on a foundation whose behaviour is known.

Inside the key decisions

What each choice cost, and what it bought.

ASplitting the first milestone

The client hired for a rebuild and expected to see new screens. Putting an access check first delays the visible work and is harder to sell, because the deliverable is an absence of problems. What it buys is a rebuild that stands on a record whose behaviour is known, with the escalation path closed before anything else depends on it. Building the screens first would have shipped a better-looking product with the same open door.

BChecking columns as well as rows

Row policies are where most reviews stop, because that is where the access rules are written. Going through every column a member can write to, and asking whether a member should be able to change it, takes longer and turns up a different class of defect. It is the only check that finds a self-escalation path hidden inside a policy that looks correct.

CAssuming exposure until it is checked

Trusting that tables are private unless shown otherwise is the comfortable default and the dangerous one, given how broadly platform defaults have exposed tables in the past. Checking what the anonymous key can actually reach means making real requests against each table with nothing but the public key. It is tedious, and it replaces an assumption with evidence.

DTreating the platform change date as a planning fact

It is easy to treat a platform's default change as a configuration detail for whoever deploys. Because the correct configuration differs on either side of the date, a rebuild that spans it can be right when tested and wrong when shipped. Putting the date into the plan costs some re-verification at the point of release. It means the configuration is checked against the platform as it will be, not as it was when work began.

Check member access
rows and columns, from a member session
Close the escalation path
plan tier no longer member-writable
Check anonymous exposure
on the right side of the change date
Rebuild the front end
on a record whose behaviour is known
Install and session handling
installed-app storage planned for
Store submission
review outcome outside this work
The rebuild was always in scope. It just moved to where it could stand on something.
FigureThe reordered first milestone. The visible rebuild sits in the middle of the sequence, after the record underneath it has been checked and the escalation path closed. Installability and store submission follow once sessions behave correctly in the installed app.
How it was measured

A single headline number hides where a system fails. This work was scored on the dimensions that actually decide whether it holds in production, measured on real, held-out cases rather than the demo path.

Self-escalation reachable from a member sessionData reachable with an anonymous keySession continuity into an installed appBehaviour across the platform change date
Self-escalation from a member sessioncan a member change what they paid for
Data reachable with the anonymous keywhat anyone with the public key can read
Session continuity into the installed appis a member still signed in after installing
Behaviour across the change dateis the configuration right on both sides
A check is a result at a point in time. Defaults change on published dates.
FigureWhat the work was checked against. No client outcome figures are published for this engagement. Each dimension is a direct question about the record or the installed app, and each answer is true at the date it was checked.
figures

Third-party scan and platform-change facts are cited with their source and date. No client outcomes are published.

What it produces
Without this discipline

A finished brand system on top of a member record where a member can promote themselves to a paid tier, and a home-screen install that silently logs everyone out.

This system

A verified member record with the escalation path closed and anonymous exposure checked, then a rebuild on top of it, with installed-app storage handled by design instead of discovered in review.

check the lock firstthe column list mattersassume exposure until checkedinstalls have their own storage
The operating envelope

What it owns, and what it hands to a person.

Handled with confidence
Member access and escalation paths
Anonymous-key exposure
Installed-app session behaviour
Flagged for review
Configuration spanning a platform change date
Out of scope by design
Store review outcomes
Payment processing itself
The honest limit

The access check covers the paths it examined and the platform defaults as they stood when it ran. Defaults change on published dates, and a check is a result at a point in time, never a standing guarantee. That is why the change date matters to the plan as well as to the configuration. Store review outcomes and payment processing itself are outside this work.

What it generalizes to

Before rebuilding the visible layer of a live product, verify the record underneath it. A redesign inherits every access defect the old interface had, and the inheritance is hard to see precisely because the interface has changed.

What to take from this

When the front end talks to the database directly, the public key really is public. Security comes from the database's policies, and nothing in the interface adds to it.

A row-level rule decides which rows a user can change. Which columns they can change is a separate question, and a plan or role column needs its own protection.

Test an access policy by making the requests the interface never makes, from a member session and from the anonymous key.

Platform defaults change on dated notices. A configuration is correct relative to a date, so record which side of each change it was checked on.

Before rebuilding a live product's interface, verify the record underneath it, because a redesign inherits every access defect and makes them harder to see.

How we engage

You have a system like this one.
Tell us where it stands.

Whether it is failing, not yet built, or about to meet a scale it has never seen, we can tell you what we see.

Start a conversation
mostafa@opulion.dev · Response within 24 hours · By inquiry