Development build¶
Expo Go cannot run this project at all, for two separate reasons: it ships only the newest Expo SDK and this project is on 54, and it cannot link native modules even when the versions line up. Three things the product depends on exist only inside a real build.
| Blocked in Expo Go | Why it matters |
|---|---|
| The native engine | modules/vlog-editor is a local module, and a local module only exists in a build that compiled it. Without one every edit falls through to the server, which is the path the whole architecture exists to avoid. |
| Remote push | Removed from Expo Go in SDK 53. Push is the trigger for the weekly loop. |
| Real permissions | app.json permissions are not applied inside Expo Go, which runs on its own. |
Setup, already done¶
eas.json, the dev-client plugin and extra.eas.projectId are all committed,
so on a fresh checkout there is nothing to configure. If you are starting again
from nothing:
That project id is also what makes push work: getExpoPushTokenAsync returns
nothing without one.
Before every build¶
Not optional politeness. The free plan allows 15 Android builds a month, each
about nine minutes plus queue time, and a duplicated native dependency or a bad
config is caught here in seconds. The native.yml CI job compiles the Kotlin for
the same reason — three builds died in a row on compile errors before it
existed.
Building¶
What a build actually does, and where each secret joins it:
flowchart TD
GIT[Committed files] --> UP[Uploaded to EAS]
ENVF["EAS file variable<br/>GOOGLE_SERVICES_JSON"] -.->|"gitignored files<br/>arrive this way"| BUILD
ENVS["EAS secret<br/>SENTRY_AUTH_TOKEN"] -.-> BUILD
UP --> BUILD[EAS builder]
BUILD --> PRE[expo prebuild]
PRE --> GRADLE[Gradle + Kotlin<br/>native module]
GRADLE --> APK[(APK)]
BUILD --> MAPS[Source maps] -->|"uploaded during<br/>the build"| SENTRY[(Sentry)]
APK --> INSTALL([Install on device])
style APK fill:#2d7d46,color:#fff
The dotted arrows are the part that catches people: anything gitignored is not in the upload, so it has to reach the builder as an environment variable. EAS warns about this and then builds anyway. See gotchas.
# Android, installable APK: about nine minutes of build, plus the free-tier queue
npx eas-cli build --profile development --platform android
# iOS needs an Apple Developer account and a registered device
npx eas-cli device:create
npx eas-cli build --profile development --platform ios
EAS returns a URL; install it on the phone. That build is a shell — it loads the JavaScript from Metro, exactly like Expo Go:
The API URL is set here rather than in eas.json on purpose: the JS is bundled
by Metro at start time, so a native rebuild is not needed to point the app
somewhere else.
You only rebuild the shell when the native side changes — a new library, a new permission, a change to the app config. Editing screens or logic does not need one.
Building on this machine instead¶
EAS is capped at fifteen Android builds a month on the free plan, and August 2026 ran out of them. Locally there is no cap, an incremental build takes about three minutes, and there is no queue.
It publishes the result to a fixed URL when it finishes, so the link to install from never changes:
https://vlogroulette-media.mintos.space/app/latest/vlogroulette.apk
That only works because the object is stored no-store and Cloudflare answers
cf-cache-status: BYPASS. A fixed URL is exactly what a CDN likes to cache, and
serving a stale APK from a link that looks right is worse than a fresh link each
time: you would install it, see none of your changes, and have no reason to
suspect the download. SKIP_PUBLISH=1 builds without uploading.
One-time setup, none of which needs root — the SDK lives in your home directory:
curl -sLO https://dl.google.com/android/repository/commandlinetools-linux-16111833_latest.zip
mkdir -p ~/Android/Sdk/cmdline-tools && unzip -q commandlinetools-linux-*.zip
mv cmdline-tools ~/Android/Sdk/cmdline-tools/latest
~/Android/Sdk/cmdline-tools/latest/bin/sdkmanager --install \
"platform-tools" "platforms;android-36" "build-tools;36.0.0"
About 490 MB, plus a JDK 17 — Expo SDK 54 wants 17 specifically, so if the
system default is newer the script points JAVA_HOME at it.
Why a script rather than a documented gradlew line. A build has to be
given the same environment EAS would give it, and those values live in
eas.json, which nothing but EAS reads. Building by hand produced an APK
pointing at localhost with error reporting switched off: entirely
correct-looking, and wrong in a way you would only discover on the phone. The
script reads the profile out of eas.json so the two routes cannot drift.
It also needs SENTRY_ORG_TOKEN in backend/.env, or the source map upload
fails the build at the very last task — which is exactly what happened the
first time.
Signing. The generated project signs release builds with Android's debug
key, which is not the key EAS uses. An APK signed differently cannot be
installed over one that is already there; Android refuses. To upgrade in place,
export the keystore from eas credentials and point the build at it. Otherwise
uninstall first, which costs the signed-in session and nothing else.
Profiles, and which one you want¶
| Profile | What it is |
|---|---|
development |
A shell that loads JavaScript from Metro on your machine, and talks to a local API. What you want while working. |
preview |
Standalone. Bundles the JS, points at the deployed API, needs nothing running. This is the one to hand somebody. |
production |
Same, as an app bundle for the Play Store. Never used yet. |
They share a package name, so installing one replaces the other. The data survives the swap, which means a stored session pointing at a server that has never heard of it — and the client does not handle 401 yet, so it looks like the app is broken. Log out first.
Profiles¶
| Profile | What it is for |
|---|---|
development |
Dev client shell, APK, loads JS from Metro. Day-to-day work. |
preview |
A standalone build with the JS bundled in, pointed at a real API. What you hand to a tester. |
production |
App bundle for the store, with autoIncrement on the version. |
preview and production carry the deployed API URL and the Sentry DSN in
eas.json. Both are baked in at build time, so changing either needs a rebuild.
Behind the door: the engine¶
The dev build is what makes the on-device edit run at all, and what it links is
modules/vlog-editor — MediaCodec and OpenGL on Android, written directly rather
than wrapping FFmpeg. On Android it is done and it runs on real hardware. On iOS
it has never been compiled, and needs a paid Apple Developer account before it
can be. roadmap.md has how it got here.
Do not add ffmpeg-kit-react-native to package.json hoping to light up the
FFmpeg path. It is deprecated and every version of com.arthenica:ffmpeg-kit-*
now 404s from Maven Central, so it resolves on npm and then fails at the Gradle
step — a slow way to learn what a curl answers in a second. That withdrawal is
the reason the custom module exists.