Skip to content
Build and run locally

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, and billing for the module graph) checked out next to luna/. The replace directives are relative (=> ../../inference from grpc/). 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/GetScore

Things to know:

  • Always pin PORT. The default is 0 (auto-assign), and native debug builds dial http://localhost:50051.
  • There is no /health endpoint. Readiness means the port answers grpcurl … list.
  • mint-session must see the same appfs root and CORE_ENVELOPE_KEK as the server. Otherwise the token is unknown and every call is Unauthenticated.
  • .claude/skills/run-luna/smoke.go is 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:50051

There 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 under lib/ needs a glyph neither font can draw, because the web engine would then fetch a fallback font from Google.
  • Components use shadcn_flutter 0.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.