Akin Shonibare

the bishop · the long diagonal

Ravine.

A map-first guide to Toronto. Every place has a person behind it and a reason it is in.

On TestFlight, October 20260.3.0SwiftUI, MapLibre, FastAPI, Postgresby coraki.dev

Ravine onboarding, Your Friday night: three photo wheels for 6 pm, 9 pm and midnight, and the sentence A sauna, comedy, then live jazz
Onboarding. 6 pm, 9 pm, midnight. The sentence writes itself.
Ravine's map of King West with photo pins for Type Books, Edulis, Nobu and the Rex, and a card for BarChef
The map. A few places at a time, and the cards follow it.
The Nuit Blanche takeover: a dark map with glowing installation pins, City Hall lit on the selected pin, and a card for Avataq at Nathan Phillips Square
A Moment. Nuit Blanche turns the whole app dark, as data.

what it is

Curated by the house.

Ravine is not a review site. 225 real Toronto places, the top twelve in each of nine categories plus the places one creator's posts led to, every one checked against two independent sources and open in 2026. The descriptions are in one voice. The map shows a few places at a time and the cards follow it.

It is built the way memo is: the same server stack, the same migration and isolation patterns, the same documents. The client is native SwiftUI rather than Expo, and that was the first decision written down.

225

places, each with hours, an address and a source beside the post

446

licensed photos, credited, served from a private bucket by version

2

Toronto Moments that re-skin the app: Nuit Blanche, the Waterfront Marathon

88

tests against a real Postgres, one of which fails if any isolation policy loosens

the look

Quiet luxury on paper.

Fraunces set light and large, deep ravine green as the ink, hairlines where other apps use fills, the platform's own glass tab bar. Ravine's own dialogs replace the system's. A Moment ships as a set of colour values and a map style, never as new components, so Nuit Blanche is a theme and the Marathon is a layer.

three moves, annotated

The decisions.

Thirteen so far, written the same way as memo’s. These three are the ones that decide how the app is built: the client, the data boundary, and what the phone loads.

1. !?ADR 0001

A native Swift app on memo's backend.

The options
Expo again, like memo, sharing a client toolchain; a web app; native SwiftUI on the server stack memo already proved.
The move
SwiftUI, for the map and the platform's own materials, on FastAPI, SQLAlchemy and Postgres with the same migration and isolation patterns as memo.
What we accepted
Two client codebases in two languages, and no shared UI with memo. Worth it for an app whose whole surface is a map.
4. !ADR 0004

Row-level security, in the database.

The options
Filter by user in every query and trust every call site, forever; Postgres row-level policies, so the database refuses what the code forgets.
The move
Policies. When following people arrived, new read-only policies opened exactly the shared tables, saves and collections, and nothing else; notes, taste and reminders stayed closed.
What we accepted
A session variable to set on every connection, and a test whose only job is to fail if a policy is loosened.
5. !ADR 0005, with 0012, photos by version

One cached catalog. Hours and moments are computed on the phone.

The options
One catalog document with an ETag, kept on the phone and refreshed on launch; conventional resource endpoints for places, curators and lists; or the server computing "open now" so the logic lives in one place.
The move
One document from one endpoint, cached raw on disk with its ETag and refreshed on launch and on returning to the foreground. The server sends each place's weekly hours and each Moment's start and end; the phone decides open, closed and live at render time in Toronto time. The phone keeps the server's bytes as sent, because re-encoding them would rewrite dictionary keys that are display strings.
What we accepted
The whole document moves when one place changes. Hours logic lives in Swift rather than Python, so there are two places it could be wrong. Past a few hundred places the document gets heavy, which is why search stays on the server as the first thing that should scale differently.
All of Ravine's decisions

what it runs on

The stack.

App
SwiftUI, XcodeGen, MapLibre with OpenFreeMap tiles
API
FastAPI, SQLAlchemy async, four workers sized by a load test
Store
Postgres with row-level security, slugs for the catalog, UUIDv7 for owned rows
Accounts
Guest first, Sign in with Apple to keep your places
Photos
A private bucket, presigned for an hour, cached on the phone by version
Gates
ruff, mypy, tests on Postgres, hand-written migrations
Next piece, the knight:Coraki, over whatever is in the way