1 · User manual
Getting in
The live archive is behind an access code here too, and the code is issued to you. Open the demo gate, leave your email, and a fresh six-digit code comes back on the spot — shown on screen and printed below, where each new code replaces the one before it. Enter it and the gate opens the archive at framesouthapp.manp.space.
Be plain about what that gate is on this lane. A static host runs no long-lived process, so the code is sealed into a signed, expiring cookie in your own browser rather than held on a server — which is why the code printed below is the one issued to this browser. And the archive it forwards you to is publicly reachable to anyone holding its address. This gate records who asked for a code; it does not restrict who gets in. It does not need to: the archive holds nothing but film metadata drawn from public, openly-licensed sources — no user accounts, no personal data, nothing to protect. The on-premises deployment is the one that genuinely closes the door, because there the application listens on loopback and is reached only through the gateway.
No access code has been issued yet. Open the gate, leave an email, and the code that comes back appears here.
The journey
- The landing page shows the archive’s most recent year at display scale, with its total release count. Earlier years appear beneath it as a selector.
- A year page lists the four industries with live release counts and links into each.
- An industry page opens with the year, the industry’s ordinal, its name, its language and its count, then a January–December timeline and the releases themselves.
- A film page carries the credits, the record, the synopsis where one exists, and the sources every fact came from.
Grid and index
Two ways to read the same list, chosen with the toggle in the control bar. GRID gives each film a typographic composition with an abstract mark derived from its own metadata. INDEX is a festival programme: number, title, genre, director, date, one line per film. The choice follows you between industries for the rest of the session.
Search
Press ⌘K or Ctrl-K anywhere, or use the Search button
in the header. It matches film titles, actors, actresses, directors, producers, music
directors, production companies and genres. The two selects narrow the search to one
year or one industry. Esc closes it.
Filtering and sorting
The month timeline filters by release month; a month with no releases in that industry is shown but cannot be selected. The Filters drawer offers genres, each with its own count. Sorting offers release date newest and oldest, and title A–Z and Z–A. Lists load twenty-four films at a time, with Load More for the rest.
Keyboard and assistive technology
Every control is reachable by tab with a visible focus ring; the first tab on any
page reaches a skip link. Each page has one h1 and proper landmark
regions. The view toggle and month filter report their pressed state. If your system
is set to reduced motion, the archive renders completely still — the grain, the
ambient light and every transition are switched off, not merely softened.
2 · A day in the life
Someone wants to know what the Malayalam industry actually released in 2025, and who made it.
- They land on FrameSouth and see the year set as artwork with a total count. They press Explore the archive.
- The year page offers four industries with live counts. Mollywood reads 216 releases. They open it.
- The industry page opens on 216 Malayalam films, newest first, twenty-four to a page. They switch to INDEX and run their eye down the whole year.
- They want March only, so they click MAR on the timeline. The list narrows; the count line follows.
- A title they half-remember catches their eye. They open it and get the cast, the director, the producers where known, the composer, the production company, the release date and a factual paragraph — with the two Wikipedia pages it was all read from, licensed and linked.
- They wonder what else that director made in 2025, so they press
⌘K, type the name, and get every film in the archive that credits them, across all four industries.
At no point did they see a poster, and at no point did the interface show them a field it could not defend.
3 · Where the data comes from
Sources
| Source | Used for | Licence |
|---|---|---|
| Wikipedia year lists | Title, release date, director, cast, production company | CC BY-SA 4.0 |
| Wikipedia film articles | Producers, music, runtime, website, genre, synopsis | CC BY-SA 4.0 |
| Wikidata | IMDb id, TMDB id, performer gender | CC0 |
Both are read through their documented public APIs — no page is scraped, no rate limit is evaded. Every request carries a contactable user-agent as Wikimedia’s policy requires, requests are spaced out, and every response is cached to disk so a repeated run costs the upstream nothing.
Why Wikipedia leads
Wikidata was measured first because it is public domain. For 2025 South Indian films it holds 268 titles and only 47 with a director. Wikipedia’s curated year-by-language lists carry a director, a cast and a production company for essentially every entry, each row citing a published source. So Wikipedia leads and Wikidata supplies the two things it is genuinely better at: stable external identifiers, and structured facts about people.
Every derivation, stated
- Release date is composed from the list’s month banner and day column. Both are read from the cell’s content only, never its HTML attributes — a lesson learned the hard way, since
rowspan="2"andbackground:#faf884both carry digits and once filed a 5 June release on 2 June. A row whose day cannot be resolved is skipped and reported, never stored with a guess. - Actor / actress / cast. Those two roles are assigned only from Wikidata’s gender property on the performer’s own item. Everyone else — 5,325 credits across the three years — is stored as
CASTand shown alongside under “starring”. Gender is never inferred from a name. - Genre is read from the article’s lead sentence, examining only the span between the language marker and the word “film”, and matching against a fixed forty-term vocabulary so a stray adjective cannot invent a genre.
- Synopsis is the first one or two sentences of the article’s lead. It is never generated, summarised or rewritten.
- Title prefers the film’s own article title over the list’s short display name — the list says “Court”, the article says “Court: State vs a Nobody”.
- Runtime is rejected unless between 40 and 300 minutes; an IMDb id unless it matches
ttplus seven to nine digits.
What is deliberately empty
Certification, OTT platform, OTT release date, trailer URL and original title are null on every row. No open source carries them reliably for these films, so the schema supports them and the interface shows nothing. This is a stated gap, not an oversight.
4 · Technical architecture
Stack
| Framework | Next.js 16 (App Router), React 19, TypeScript in strict mode with no unchecked index access |
| Styling | Tailwind 4 plus one hand-written token layer; three self-hosted typefaces, no font CDN |
| Database | PostgreSQL 16 through Prisma 7 with the pg driver adapter |
| Pipeline | TypeScript scripts run with tsx: research, validate, import, fixture |
| Tests | Vitest for units and integration, Playwright for the browser |
Data model
Ten tables: years, industries, movies,
people, movie_credits, genres,
movie_genres, production_companies,
movie_production_companies and movie_sources. Credits are
relational with a role, so one person can be a director and a producer on the same
film. A film’s natural key is (year, industry, source key), which
is what makes a re-import an update and makes a cross-industry collision impossible.
Rendering
Every year page, industry page and film page is prerendered — 891 static pages — and revalidates every five minutes. Because the set of valid years, industries and film slugs is finite and known, the dynamic parameter sets are closed: anything outside them is a real HTTP 404 rather than a not-found page served with a 200. The archive browser hydrates on top of server-rendered content, so a list is readable before any JavaScript runs.
The three mechanical guarantees
A promise that is only prose decays. Each of these is a check that fails the build:
- No imagery — no
<img>, nonext/image, nourl()pointing at a file, and no raster asset anywhere in the repository. The linter blocks an<img>at the syntax level too. - No hardcoded year — no literal year decides behaviour in the application source.
- One palette — colour literals exist in exactly two files, the token sheet and the single value the browser reads before CSS parses.
A fourth check computes every ink-on-ground and accent-on-ground contrast pair against WCAG AA and fails below threshold. All fifteen pass.
5 · API reference
All endpoints are read-only JSON. Every parameter is validated before it reaches the
database; a bad one returns 400 with a body of exactly
{"error": string}, and no error body ever contains a stack trace, a
table name or a filesystem path.
GET /api/years
{ "years": [ { "year": 2025, "isActive": true, "isFixture": false, "count": 894 },
{ "year": 2024, "isActive": true, "isFixture": false, "count": 907 } ] }
GET /api/industries?year=2025
The four industries with their release counts for that year. An unknown year returns 404.
{ "year": 2025, "industries": [
{ "slug": "tollywood", "name": "Tollywood", "language": "Telugu",
"cinema": "Telugu Cinema", "displayOrder": 1, "accent": "tollywood", "count": 184 }
] }
GET /api/movies
| Parameter | Accepts |
|---|---|
year | a four-digit year the archive holds |
industry | tollywood, kollywood, mollywood, sandalwood |
language | Telugu, Tamil, Malayalam, Kannada |
genre | a genre slug, e.g. thriller |
month | 1–12 |
search | free text, up to 120 characters |
sort | date-desc, date-asc, title-az, title-za |
limit | 1–100, default 24 |
offset | 0 or greater |
GET /api/movies?year=2025&industry=tollywood&month=3&sort=title-az
{ "total": 17, "limit": 24, "offset": 0, "nextOffset": null,
"items": [ { "id": 1, "slug": "court-state-vs-a-nobody-2025-tollywood",
"title": "Court: State vs a Nobody", "releaseDate": "2025-03-05",
"releaseMonth": 3, "language": "Telugu", "industry": "tollywood",
"genres": ["Drama","Legal"], "directors": ["Ram Jagadeesh"],
"leads": ["Priyadarshi Pulikonda","Sivaji"], "music": ["Vijai Bulganin"] } ] }
List items deliberately carry only what a card prints. Full credits, the synopsis and the sources live on the single-film endpoint.
GET /api/movie/:idOrSlug
One film with its complete credits, production companies and sources. Accepts either the slug or the numeric id. An unknown film returns 404.
GET /api/facets?year=2025&industry=tollywood
The genres, months and languages that have records, each with its count. A value with zero records is absent.
6 · Adding a year
Adding a year is data work, not engineering work. This has now been demonstrated twice — once with a throwaway 2099 fixture, and once for real when 2024 was added after 2025 had shipped. Both times every file under the application directories hashed identically before and after, while the new year appeared automatically in the year list, the routes, the counts, the search and the facets.
npm run movies:research -- --year=2026 # harvest into data/2026/*.json
npm run movies:validate -- --year=2026 # reject with named reasons, no silent drops
npm run movies:import -- --year=2026 # import and activate
npm run build # prerender the new routes
vercel deploy --prod # publish the cloud lane
The cloud lane keeps its own copy of the database, so a new year is imported there too — either by re-running the importer against the cloud connection string or, far faster for a full archive, by restoring a dump of the local one.
The importer prints found, valid and rejected per industry with a reason for every rejection, and re-running it updates rather than duplicating.
7 · Operations
The same codebase runs in two places, switched by environment variables alone: on Vercel at framesouthapp.manp.space against a Neon PostgreSQL instance, and on premises under systemd against a project-owned cluster. Nothing in the application knows which lane it is in beyond its connection string.
| Service | Port | Faces |
|---|---|---|
| Vercel production | 443 | public — the cloud lane, Mumbai region |
framesouth-app | 8002 | loopback only; reached solely through this gateway |
betadoc-framesouth | 6796 | this site, the access gate and the proxy |
phasedoc-framesouth | 6795 | internal engineering window, never published |
| PostgreSQL | 5433 | project-owned cluster; the machine’s system cluster is untouched |
The gateway serves the application at its own natural URLs rather than under a path
prefix. A prefix was built first and abandoned: Next.js constructs links on the
client from data, so rewriting the response cannot reach them and the first in-app
click escapes the prefix. The gateway therefore reserves six paths — /
before sign-in, /documentation, /demo,
/leave, /healthz and /style.css, plus
/media/ — and hands everything else through untouched. Once you are
signed in, / is the archive.
All three run under systemd with restart-on-failure. Secrets live in gitignored environment files: the database URL in the application’s, the cookie signing key in this gateway’s. The gateway also keeps the code it last issued in a gitignored file readable only by its own account, so a restart neither invalidates the code a visitor is holding nor leaves this page printing a dead one. A production build was checked to carry no connection string into the browser bundle.
8 · Known limits
- Credit depth follows the source. Producers, music directors, runtimes and synopses only exist for films that have their own encyclopaedia article — 1,203 of 2,359 carry a synopsis, 739 a runtime. The rest show no such row rather than an empty one.
- Half the performer credits have no verified gender. 5,325 of 10,684 are stored as cast. That is a limit of the source, and preferable to guessing.
- Five fields are empty everywhere. Certification, OTT platform, OTT release date, trailer and original title are modelled and unpopulated.
- Cast order is source order, not billing order. The lists do not state billing, so neither do we.
- Three years. The archive holds 2024, 2025 and 2026. The architecture for more is built and demonstrated three times; the research for 2023 is not done.
- English-language titles. Titles are as the English-language sources give them; native-script titles are modelled as
originalTitleand not populated.