One rigid grid became four filterable views. Free text became real dates.
Drafts became invisible at the database rather than in React. A rebuild of
/projects on sankettambare.in, and the three bugs it uncovered on the way.
The old /projects rendered its rows verbatim through a fixed
six-variant layout keyed off index % 6, in whatever order a hand-maintained
integer said.
sort_order was 0 on nine of thirteen rows2020-10-20 with August 2026 — unsortableThe page was not slow or ugly. It simply had no data worth querying, so nothing could be asked of it.
The redesign was blocked on the schema, so the schema went first.
sort_order — hand-maintained, mostly zero, and replaced by
ordering on a real date. Nothing reintroduces it.
image became nullable. A project now falls back to its first
screenshot, then to a placeholder.
Tech stack is a plain text[], not tags.
The central tag RPC replaces an entity’s entire tag set, so it cannot back two
independent tag fields on one row. Tags stay the topical axis; stack is the build axis.
A project can be toggled to draft in the admin. Filtering that out in React would be theatre — the row would still sit in the JSON payload, readable in devtools.
create policy "public read" on public.projects
for select using (visible or public.is_owner());
13 rows · sees drafts
12 rows · no drafts
12 rows · no drafts
Verified three ways, including through the cross-entity tag RPC —
a hidden project also drops out of its /tags pages, its share card, and the
public project counts on the homepage. It is one policy, so it cannot be forgotten
at one call site.
Thirteen rows held two formats. Sorting them meant normalising every value, then changing the column’s type underneath.
text not null2020-10-20August 2026July 2026Sorted alphabetically. “August” before “July”.
date, nullable2020-10-202026-08-012026-07-01Sorted chronologically. Undated rows sink to the bottom, in both directions.
Nullable on purpose: “no date” is missing information, not an extreme value, so it never sorts as the oldest or the newest thing you own.
Tabs carried in the URL, the way the Treks and Sports pages already work.
Featured spotlight over a masonry grid. Cards size to their content instead of to their position.
A chronological spine grouped by year — only possible once the dates were real.
Per-year, per-stack, per-category and per-status breakdowns of whatever is filtered.
Dense and sortable. The fastest way to take in everything without scrolling a gallery.
All four read from one filter hook, so a filter set in Showcase survives
a switch to Table — and both ?view= and every filter live in the query string,
which makes any filtered view a shareable link.
Picking React and Supabase widens the stack axis. Adding a category on top of that still narrows. It is the behaviour people expect and rarely get.
15 chips across 12 public projects.
Central tags finally appear here too. Projects had carried them for a full version already — the old page simply never rendered them.
prefers-reduced-motionThat last one was a real bug in waiting: a falsy src does not
reliably fire onError, so the old fallback would have rendered an empty box the
moment cover images became optional.
The admin is schema-driven, so new fields are usually declaration only. Three things genuinely did not exist yet.
Repeatable {label, url} rows with reorder and delete — repo,
demo, write-up. Modelled on the existing image-slides widget.
A field can suggest from values already used on its own resource, so the stack field proposes stacks you have used without touching the tag tables.
A field can declare a default. Without it, visible would start false
and every new project would be born hidden.
That third one is the kind of bug that ships quietly and is blamed on the database three weeks later.
New columns are worthless empty. Every project’s live site and repository was opened and read, and the stacks were taken from evidence rather than memory.
<meta name="generator"> → Astro v7.2.8/_next/static/chunks/app/ → Next.js App Router_astro/ and data-astro in the shellFour lines carry a REVIEW marker in the SQL, where the source
was a README rather than a stated fact. Inference is allowed; passing it off as
evidence is not.
One row is stored as " E20 ka Chakravyuha". Every
where title = … in the date backfill missed it, so the row fell through
to the non-ISO safety net and had its date cleared. Titles are trimmed
first now, and matched on btrim.
The cross-entity tag function declares date text and passed the column
through raw. That type-checked only while the column was text — changing it
would have broken every tag page. Cast in the same migration.
Both were caught by rehearsal, not by reading. Every migration was applied to a scratch PostgreSQL instance loaded with the real thirteen rows before it touched anything live — which also proved the whole chain applies clean from empty, and is idempotent.
Website 4 · Web App 4 · Tool 2 · Design 1 · Data Story 1. Spanning 2020 to 2026.