LagosPhoto
Full Public Site & CMS for an International Arts & Photography Festival
Key Technical Highlights
Edition-based data spine — nearly every content type carries a nullable edition FK, with a derived tag computed identically across the API from one shared pattern driving all site-wide filtering
Dual-strategy search built directly on Postgres extensions — pgvector semantic embeddings plus pg_trgm trigram 'did you mean' — with zero extra search infrastructure
Migrated an entire festival's content history (32 artists, 37 exhibitions, ~100 events, 12 programmes, 19 press releases) from flat HTML into a normalized 15+ table schema via a scrape → transform → seed pipeline
Built a full open-call-to-artist automation pipeline, then deliberately and cleanly reverted it once its complexity wasn't worth the actual product value
Two-tier Postgres backup/DR — six-hourly self-verifying dumps plus continuous WAL archiving — validated with a real, timed restore drill
The Case Study
Overview
LagosPhoto is a full public site and CMS for an international arts and photography festival — a FastAPI backend and Next.js frontend in one repo, backed by Postgres with the pgvector extension. Solo-designed and solo-built: schema, backend, frontend, VPS hardening, CI/CD, backup and disaster recovery, and a full technical and growth SEO program.
The Festival as the Spine of the Data Model
Nearly every content type — exhibitions, artists, events, programmes — carries a nullable "edition" (festival year) foreign key, with a derived tag computed identically across every API response from one shared pattern. That single relationship drives essentially all site-wide filtering, rather than a status flag scattered across tables. Location data lives only on venues, never duplicated onto exhibitions or events, with every consumer joining through the venue relationship — a rule stated twice in the project's own conventions after apparently being a source of past bugs.
Search Without New Infrastructure
Rather than reaching for an external search service, LagosPhoto runs a dual-strategy search directly on Postgres extensions: pgvector for semantic embedding search, and pg_trgm trigram matching for a "did you mean" fallback against a curated name/title dictionary — with result ranking explicitly boosting the currently active festival edition over archive content.
Migrating a Festival's History
An entire legacy site's content — 32 artists, 37 exhibitions, roughly 100 event pages, 12 programmes, and 19 press releases — was migrated from flat HTML into a normalized 15+ table schema through a scrape → transform → seed pipeline, sequenced by foreign-key dependency so a failed name-match couldn't cascade into a hard stop. The source data had no venue coordinates at all, but the new schema requires them for the map view — solved with a neighbourhood-keyword lookup table and an explicit fallback flag for any address that couldn't be resolved automatically.
Knowing When to Reverse Course
An early feature auto-generated secure-token draft-submission portals and auto-created artist profiles directly from an accepted open call. Once its complexity was judged not worth the actual product value, it was cleanly and completely reverted — every router, service, schema, frontend page, and nav entry removed, a migration dropping the now-unused table, and closing verification (import checks, a clean typecheck, a dangling-reference grep) — executed to the same standard as the original build.
Infrastructure
Zero-downtime deployment via an expand/contract migration pattern, Cloudflare DNS-01 certificate challenges, and a backup system that keeps PII-bearing database dumps in a private bucket, separate from the public CDN-fronted media store. On the growth side: ten-plus JSON-LD structured data types validated against Google's Rich Results tool, a dynamic sitemap, environment-driven canonical URLs, and a full off-page SEO operating model.