A share link that has to forget who you are
My garden app is multi-tenant the lazy way, which is also the good way: every per-tenant table has a garden_id column, and Postgres row-level security filters it. The web layer sets one GUC per request and about eighty db.q(...) call sites keep working unchanged.
CREATE POLICY tenant_isolation ON seed_pods
USING (garden_id = nullif(current_setting('app.current_garden', true), '')::int)
WITH CHECK (garden_id = nullif(current_setting('app.current_garden', true), '')::int);
Note what happens with no GUC set: current_setting(..., true) is NULL, garden_id = NULL is NULL, and the policy passes nothing. Unauthenticated means zero rows. That is exactly the behaviour you want — right up until you want a public link.
The bootstrap problem
The feature is a read-only page anyone can open: here's the seed tray, here's what has come up, here's the day each one broke ground. No login, no account. So the request arrives with a token and nothing else, and the server has to answer "which tenant is this?" before it can bind a tenant and read anything.
If the tokens live in a tenant-scoped table, they're unreadable to the very request that needs to read them. So that one table deliberately isn't tenantized:
CREATE TABLE seed_box_shares (
token TEXT PRIMARY KEY,
garden_id INT NOT NULL REFERENCES gardens(id) ON DELETE CASCADE,
box_id INT REFERENCES seed_boxes(id) ON DELETE CASCADE,
revoked BOOLEAN NOT NULL DEFAULT false
);
It's global on purpose, it carries its own garden_id, and every query it feeds is then scoped to that garden explicitly rather than to whatever the visitor's cookie happens to say:
sh = db.q("SELECT * FROM seed_box_shares WHERE token=%s AND NOT revoked",
(token,), one=True) # no tenant bound yet
boxes = db.q("SELECT * FROM seed_boxes WHERE NOT archived",
garden_id=sh["garden_id"]) # pinned to the share's tenant
The rule I'd write on the wall: an un-tenantized table is fine as a key — token to tenant — and never as a store. Nothing about the trays themselves lives there, so a bug in this table leaks a mapping, not data.
Two details that turned out to matter:
Foreign keys still work. box_id references a table with FORCE row security on. That's fine — Postgres runs referential-integrity checks with row security bypassed, so the FK doesn't need to see the row through a policy.
One live link per thing. A partial unique index makes re-sharing idempotent, so pressing Share twice hands back the same URL instead of scattering live tokens:
CREATE UNIQUE INDEX ux_shares_live ON seed_box_shares (garden_id, COALESCE(box_id, -1))
WHERE NOT revoked;
COALESCE(box_id, -1) because NULL means "all trays" here, and NULLs don't collide in a unique index — without it, "share everything" would mint a fresh token every press.
The link preview lies unless you render it
The page draws itself client-side from a JSON endpoint. Link-preview crawlers don't run JavaScript, so a shared link showed the filename and nothing else. Cheap fix: substitute the title and description server-side before handing over the HTML, leave the body to the client.
page = open("share.html").read()
return HTMLResponse(page.replace("{{TITLE}}", escape(title))
.replace("{{DESC}}", escape(desc)))
Now the preview reads "15/25 sprouted" instead of a URL, which is most of the reason to send it to anyone.
The 12-column screen
Same feature on the phone, different failure. The tray view has to keep the tray's real shape — a 6×6 tray is six wide, you can't reflow it — so on a narrow screen it scrolls sideways. In SwiftUI that's a horizontal ScrollView inside the vertical one, and it reports its content's width as its ideal width. The whole screen went wider than the display and every other row got shoved off the left edge.
Pinning the inner scroller to a measured width fixes it. But measure the wrong thing and you get a feedback loop: a wide tray widens the card, the card reports a wider measurement, the tray stays wide. The measurement has to come from an ancestor that doesn't depend on the content — the tab's own geometry, handed down:
GeometryReader { geo in
ForEach(trays) { tray in
TrayView(tray, innerWidth: geo.size.width - 60) // insets, known
}
}
Fixed size in, fixed size out. The general shape: if a view's size depends on a measurement it also influences, you don't have a layout, you have a loop that happens to converge somewhere ugly.
What I'd do differently
I built the tenant-scoping first and the tile design second, and the tile design was where the actual complaint was — the old cells were an 8pt dot and six characters of the seed's name, which told you neither what was in a pod nor whether it had come up. The share link is the fun engineering. Being able to read your own screen is the feature.