In Progress
22 August 2026

Sledi

An Android TV and movie tracker built to replace TV Time after it shut down — a Postgres-backed sync layer, a shared metadata cache, and an importer that rescues years of watch history.

React NativeExpoTypeScriptSupabasePostgreSQLRow-Level SecurityTanStack QueryTMDB APIEAS
Sledi

Infrastructure

There is no application server. The client talks directly to Postgres through Supabase, and the parts that need privilege live in the database as SECURITY DEFINER functions — six of them, covering show and movie sync, the home payload, profile statistics, saving watch feedback and recomputing rating aggregates.

That choice shapes everything else. sync_show takes pre-fetched TMDB data as JSON and upserts the show and its entire episode list in one transaction, so a sync either lands completely or not at all. The media and episodes tables are readable by every authenticated user but writable by nobody directly, which makes that function the only door into the shared cache. Because the cache is shared rather than per-account, one person opening a show refreshes it for everyone, and re-sync intervals are tuned by state: two days for a returning series, fourteen for one that has ended.

Two Deno edge functions handle what should not run client-side — a GDPR data export and a hard account delete. Both identify the caller from their JWT and run every query through that token, so row-level security constrains the result and a client-supplied user id is never accepted. Builds go out through EAS as signed APKs published to a public GitHub repository, which the app checks for new releases itself, sidestepping store review entirely.

Built on open source

The database is ordinary Postgres, reached over plain SQL with no proprietary query layer in between. Supabase supplies the auth, storage and hosting around it, but the schema is eighteen numbered migration files that would apply to any Postgres instance, and local development runs the same engine as production — nothing in the data model is hostage to the hosting choice. The app returns the favour with a licence browser in its settings, listing all 675 open-source packages in the production bundle, generated by a script that crawls the dependency tree so it cannot drift out of date.

Implementation notes

Scope was locked to six features before I wrote any code, and the hard part was leaving it locked — a social feed, comments and recommendations all came up and were all turned down, because the deadline belonged to someone else. Tests went only where the logic is pure and being wrong is expensive: the export parser, the air-date rules, the matcher, the library bucketing. Those run as plain functions over fixture data with no device or network involved. The UI has no test coverage at all, deliberately.

Problem & solution

Problem

TV Time shut down on 15 July 2026 and took years of watch history with it. Other trackers could follow shows, but none could ingest the export TV Time handed users on the way out, so switching meant abandoning a decade of viewing history and starting from an empty list.

Solution

An Android-first tracker built on Postgres and Supabase, with the import as the headline feature. It reads the TV Time archive on the device, fuzzy-matches every title against TMDB, and routes anything ambiguous to a confirmation screen instead of guessing.

Highlights

One-tap episode logging, with the next unwatched episode surfaced automatically

A home screen that sorts itself into Up next, Watching, Haven't watched lately, Caught up and Watchlist

Calendar of upcoming episodes and movie releases, with opt-in notifications per category

Lists with tags and visibility, favourites, and a searchable, sortable library

Ratings, reactions and private notes, plus community averages plotted against TMDB scores

TV Time import: archive parsed on-device, fuzzy-matched to TMDB, manually confirmed

My role

Full-stack mobile

Built solo — the React Native app, the Postgres schema and every row-level security policy, the server-side sync functions, the TV Time import engine, and the release pipeline.

Key decisions

01

Postgres functions as the API surface

Instead of a bespoke server, the write paths live in Postgres as SECURITY DEFINER functions. Syncing a show upserts metadata and every episode inside a single plpgsql transaction, so there is no such thing as a half-synced show, and re-running it is idempotent.

02

One shared metadata cache for every user

TMDB metadata is not duplicated per account. The media and episodes tables are readable by any authenticated user and writable only through the sync functions, so the first person to open a show warms the cache for everyone.

03

Supabase over a hand-rolled backend

Postgres, auth, storage and edge functions in one managed service. The database underneath is plain open-source Postgres reached over standard SQL, so the schema stays portable and the stack can be run locally or self-hosted rather than being locked to a vendor.

04

The importer asks instead of guessing

A wrong automatic match silently corrupts years of history. Anything the matcher is not confident about goes to a review screen, which makes the import slower but leaves the user in control of their own data.

Challenges & fixes

The TV Time export is not clean data

The archive is CSV files with escaped quotes, newlines inside fields, and watch records spread across several files. On top of that, TV Time identifiers do not map onto TMDB, and titles alone are ambiguous — remakes and reboots collide, and one bad match pollutes a whole show's history.

The parser and history merge are pure functions with no I/O, so the ugly cases are pinned down by Jest tests over real export fixtures. The matcher scores candidates on title, year and episode count, auto-accepts only high-confidence results, and sends the rest to a review screen. The archive is read entirely on the device — nothing uploads until the user confirms.

TMDB has holes in episode air dates

Episodes in the middle of a season sometimes carry no air date at all, which broke the has this aired yet check that both the calendar and the import reconciliation depend on.

An undated episode counts as aired if any later episode of the same show has aired. It is a forward scan rather than a per-episode lookup, and it holds up across the gaps in TMDB's data.

The home screen took a waterfall of queries to draw

Rendering the home screen meant fetching the library, then the watch history, then the episodes needed to work out what was next — a chain of dependent round trips before anything appeared, on every launch.

A single Postgres function returns the library, scoped history and a lean episode set as one JSON payload, and the result is persisted to disk so a cold start renders instantly from the last snapshot and reconciles in the background.

Community ratings without exposing anyone

Plotting a crowd average beside the TMDB score meant every user reading rating data that belongs to other people — exactly the kind of feature that leaks more than it intends to.

A separate aggregate table stores only identity-free sums and counts, recomputed server-side, while the personal ratings behind them stay locked under row-level security. Only the most recent rated watch per user counts, so rewatches cannot stuff the average.

Results & impact

7 weeks

from first commit to public early access

18

SQL migrations — the only way schema ships

675

open-source packages credited in-app

Project gallery

Sledi gallery 1
Sledi gallery 2
Sledi gallery 3
Sledi gallery 4
Sledi gallery 5
Sledi gallery 6
Sledi gallery 7
Sledi gallery 8
Sledi gallery 9
Sledi gallery 10
Sledi | Nejc Podlogar