Roadmap¶
What is deliberately not being worked on, and why — written down so the order is a decision rather than whatever came up last.
The product's central claim, that the phone does the editing and the server stays light, has executed: on real hardware, in production, from a real phone.
Done, and why they were first¶
Kept here rather than deleted, because the sequence explains the shape of the code and because "why is it like this" is the question a newcomer asks.
- Development build. Expo Go cannot link native modules, so it could never
run the real edit. The dev build unblocked three things at once: the native
engine, push notifications — the trigger for the whole weekly loop — and the
real permission prompts, which
app.jsondoes not apply inside Expo Go.
Expo Go is now not merely limited but unusable here: it ships only the newest SDK and this project is on 54.
- The native renderer. Done, and running on real hardware.
mobile/modules/vlog-editoris a local Expo module: on AndroidMediaExtractorandMediaCodecwith an OpenGL composite in between, on iOSAVMutableComposition.
It went in two stages, and the distinction still explains the shape of the
code. Stage 1 joined takes without decoding — fast, lossless, and with no
codec in the loop at all. Everything it could not do traced back to that one
absence: no silence trimming (finding silences means decoding audio), no
crossfades, no loudness normalisation, no stills (MediaExtractor cannot
open a JPEG), no mixing formats.
Stage 2 was therefore an encoder, not a list of features. Decode, do the work, re-encode — which unlocked all five at once. The Android module now reports every capability as supported, so photos and mixed formats no longer send an edit to the server.
What remains: iOS has never been compiled, and the Swift side is behind — no audio analysis, no stills, no fades, no loudness. It needs a paid Apple Developer account before it can even be built.
The engine picker still routes around whatever an engine says it cannot carry, and the edit screen says which limitation applies — distinguishing "this phone has no engine" from "this material needs one", so nobody goes hunting for a fault in a device that is working fine. That machinery earns its place the moment iOS exists.
- Decide with numbers. The question that justified all of it: a two-minute vlog assembling in 40 seconds is viable, four minutes and a hot phone is not. It assembles, and nobody has sat down and measured it properly — see Judge the edit below.
The FFmpeg problem, and how it was answered¶
ffmpeg-kit-react-native was the library the on-device pipeline was originally
written against. It is deprecated on npm and its native binaries are gone:
every version of com.arthenica:ffmpeg-kit-* returns 404 from Maven Central and
the artifact directory no longer exists. The community fork resolves the same
coordinates, so it inherits the problem.
The answer was the custom module — MediaCodec on Android, AVFoundation
on iOS — which is why modules/vlog-editor exists and why the engine picker
still has an unreachable ffmpeg branch in it. The options that were weighed:
- Host the AARs. ffmpeg-kit is open source; build it and publish the artifacts to a private Maven. Most faithful to the code that already exists, and the EDL and command builders would work unchanged.
- A custom Expo module over
MediaCodec(Android) andAVFoundation(iOS). More work, far smaller binary, no dependency on an abandoned project. The hard half — the EDL — is already pure TypeScript and tested, so the native side only has to execute cuts, crossfades and loudness. - Concede the server render. Cheapest to build, most expensive to run, and it contradicts the reason the architecture was chosen. Worth measuring before ruling out, not before trying the other two.
Next: the things a real deployment needs¶
- ~~CI.~~ Done.
.github/workflows/ci.ymlrunsruff,pytest,tsc,jestandexpo-doctoron every push, with coverage thresholds that fail the build, plus a non-blocking dependency audit.
.github/workflows/native.yml compiles the Kotlin module, and only runs when
native files change. It is the one that pays for itself: three EAS builds died
on compile errors in a row, nine minutes and a build from the quota each, for
mistakes a compiler names in five.
What the coverage number does not include, which matters more than the
number itself: the Kotlin engine has none. Native code needs instrumented
tests on a device or emulator, and it is currently the riskiest part of the
project -- every one of the last few defects came from there. On the client,
only the pure modules are measured; screens and the renderers that talk to
FFmpeg, the filesystem and the network are outside it on purpose, since
including them would produce a number nobody could act on.
- ~~Rate limiting.~~ Done: app/api/ratelimit.py covers login,
registration, joining a circle and the upload routes. Reading is untouched.
A startup guard also refuses to run in production on the development signing
key.
- The rest of the auth story. Access tokens last 30 days with no refresh and
no revocation, so a stolen one is good for a month, and there is no password
reset -- a forgotten password is a lost account. Both turn into support
problems the moment the users are not you.
- ~~A deadline reminder.~~ Done: the vlogger is warned 24 h out. Before it,
the only notification about a deadline was the one saying it had passed —
somebody could be removed from a circle for three strikes having never been
told the clock was running.
- ~~Somewhere to see past vlogs.~~ Done. The API always returned them; the
app never asked.
- Judge the edit. Nobody has sat down with a finished vlog and decided
whether the fades, the loudness matching and the four-second photo hold are
right. It assembles and the numbers are fine; whether it looks good is a
product judgement and it has not been made. Nor has anyone measured how long a
full-length vlog actually takes on a mid-range phone.
- Somebody other than the author using it. Still the largest untested thing
in the project, and the only one that no amount of engineering closes. It is
also the only way to verify a notification arriving on a phone that is not the
one that sent it.
- ~~A public bucket domain rather than r2.dev.~~ Done: video is served
from vlogroulette-media.mintos.space, so Cloudflare's rate-limited
development hostname is out of the path.
Deployment¶
Live since 2026-08-21: one Fly.io machine in Amsterdam, Postgres on Neon, video
in Cloudflare R2 behind its public bucket domain. The whole loop has run against
it from a real phone. The steps, and what running as exactly one machine
implies, are in deployment.md.
Fly's own Managed Postgres was tried and dropped: the smallest plan is $38 a month, which buys reliability this does not need yet.
This replaced a Raspberry Pi behind a Cloudflare tunnel. The tunnel meant the
origin could not be reached directly, which is why the rate limiter used to
believe X-Forwarded-For; on a host with a public address that assumption is
worth nothing, and the header to trust is now named explicitly by
CLIENT_IP_HEADER.
Backups and error reporting are done -- scripts/backup.py on a systemd user
timer, and Sentry, which is also where a failed backup reports. The honest gap
left is where the copy lives: a laptop switched off for a fortnight backs up
nothing for a fortnight, and nobody is actively watching the errors either. Both
were written for the day the users are not just the person who can read the
logs.
Later: product¶
Deliberately parked. None of them is blocked on anything now that the edit runs on the device; they are simply worth less than the items above.
- Reactions or comments on a premiere.
- A history of past vlogs, beyond the 24 h window.
- Streaks and standings on top of the existing strike count.
- A music bed. The pipeline supports one; what is missing is licensing, not code.
- Choosing how long each imported photo is held, rather than a fixed four seconds.
Known compromises¶
- Gallery import contradicts the PRD, which rules out the camera roll to force authenticity. Added on request; imported items are tagged in the manifest so the decision stays visible.
expo-file-system/legacy. SDK 54 moved the classic API there and the app still uses it, because it is the only one exposinguploadAsync— the streaming upload that made raw takes survive a mobile connection.expo-image-pickerdeclares legacy storage permissions of its own (READ_EXTERNAL_STORAGE,WRITE_EXTERNAL_STORAGE). They only apply on Android 12 and below, where the system photo picker does not exist and the picker genuinely needs them. Removing them would break gallery import on older phones.- The scheduler runs in-process. One API instance only. Fine for the MVP, wrong the moment there are two.