Build and run locally
Prerequisites
- Go 1.26 and Flutter 3.44.0 (Dart 3.12.0), the same versions as the image.
- The sibling platform libraries (
inference,mesh,security,store,ui, andbillingfor the module graph) checked out next toluna/. Thereplacedirectives are relative (=> ../../inferencefromgrpc/). The Flutter app also takes the shared UI foundation as a path dependency (../../ui/foundation/flutter). - Optional:
grpcurl, to poke at the API.
No model, database or native library is needed to boot. Inference, voice
and TTS are remote services, and each one reports Unavailable when it isn’t
configured.
Build and verify
cd grpc && go build ./... && go vet ./... && go test ./...
cd flutter && flutter pub get && flutter analyze --no-fatal-infos && flutter test--no-fatal-infos is needed because the generated luna.pbgrpc.dart carries
one info-level lint. Warnings and errors are still fatal. CI runs both halves
on every PR (.github/workflows/ci.yml). The Flutter job refuses a false
pass: it fails if analysis walked nothing, or if fewer tests ran than
MIN_TESTS.
Run the backend headless
Google Sign-In can’t be done headless, so a local run opens a session with the server binary itself.
cd grpc && go build -o bin/luna-server .
# appfs has no unencrypted mode: no KEK means no session store.
export CORE_ENVELOPE_KEK="$(openssl rand -base64 32)"
export LUNA_APPFS_DIR=/tmp/luna-appfs # local encrypted root, durable with no object store
export LUNA_OWNER_EMAIL='you@example.com'
PORT=50051 ./bin/luna-server serve > /tmp/luna.log 2>&1 &
until grpcurl -plaintext localhost:50051 list >/dev/null 2>&1; do sleep 1; done
export LUNA_SESSION_TOKEN="$(./bin/luna-server mint-session --account you@example.com)"
grpcurl -plaintext -H "authorization: Bearer $LUNA_SESSION_TOKEN" \
localhost:50051 luna.v1.HabitService/GetScoreThings to know:
- Always pin
PORT. The default is 0 (auto-assign), and native debug builds dialhttp://localhost:50051. - There is no
/healthendpoint. Readiness means the port answersgrpcurl … list. mint-sessionmust see the same appfs root andCORE_ENVELOPE_KEKas the server. Otherwise the token is unknown and every call isUnauthenticated..claude/skills/run-luna/smoke.gois a stdlib-only driver (go run .claude/skills/run-luna/smoke.go 50051). It lists the services, calls WhoAmI and GetScore, checks that an unauthenticated call is rejected, then logs the session out and checks that the token is refused.- Kill the server by PID, not with
pkill -f 'luna-server serve', because that pattern also matches the shell running it.
To make chat work, point INFERENCE_REMOTE_URL at a mistral.rs plane. Vision
needs LUNA_VISION_REMOTE_URL, semantic memory needs LUNA_EMBED_URL, and
voice needs VOXTRAL_REMOTE_URL and TTSD_URL. See
Configuration.
Run the app
cd flutter
flutter run -d web-server --dart-define=LUNA_BACKEND_URL=http://localhost:50051There are exactly two build defines, LUNA_BACKEND_URL and
LUNA_GOOGLE_CLIENT_ID. There is no debug auto-login: the login screen
has only the Google button, so a real sign-in needs an allowlisted account
and an OAuth client whose authorised origins include the one you serve from.
The client refuses plaintext transport except to loopback. Release and
profile native builds default to https://luna.runink.org, and debug builds
default to http://localhost:50051.
Regenerate the protobuf stubs
The generated Go and Dart code is committed, so regenerate only after editing
grpc/api/proto/luna/v1/luna.proto. Run from the repo root, where
buf.work.yaml lives:
buf generate --template grpc/buf.gen.go.yaml --path grpc/api/proto # Go: grpc/api/v1/...
buf generate --template grpc/buf.gen.dart.yaml # Dart: flutter/lib/core/grpc/...This needs protoc-gen-go, protoc-gen-go-grpc and protoc-gen-dart on
$PATH. --path grpc/api/proto is required on the Go run. luna.proto
imports the shared runink.ui.judgement.v1 messages from a byte-identical
snapshot in third_party/ui-judgement/, and their Go types come from the
ui module, so they must not be generated here. A test fails if the
snapshot drifts from ui.
Release builds
Build store artefacts only with:
flutter/tool/build_release.sh appbundle # Android .aab
flutter/tool/build_release.sh ipa # iOS (needs a Mac; never compiled yet)The script adds --obfuscate --split-debug-info and writes the symbols to the
gitignored /symbols/. Archive them for every release, because they are the
only way to read a crash. R8 (Android) and symbol stripping (iOS) are pinned
on. Ship the .aab, not a fat APK.
Conventions worth keeping
- Fonts are vendored (Roboto and NotoSansSymbols). Never add
google_fonts, because it fetches fonts at runtime. A test fails if a string underlib/needs a glyph neither font can draw, because the web engine would then fetch a fallback font from Google. - Components use
shadcn_flutter0.0.53, pinned. The bespoke painters (the creature, starfield, streak chips, chat bar) deliberately stay custom. - New streaming RPCs get serialized sends from the interceptor. They still have to beat a heartbeat if they wait on the model.
- Never add a password path, and never add a third-party AI SDK or endpoint.