Skip to content

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:

cd mobile
npx eas-cli login
npx eas-cli init          # writes extra.eas.projectId into app.json — commit it

That project id is also what makes push work: getExpoPushTokenAsync returns nothing without one.

Before every build

npx expo-doctor

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:

EXPO_PUBLIC_API_URL=http://<your-lan-ip>:8000 npx expo start --dev-client

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.

scripts/build-android.sh preview

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.