Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
5.2 KiB
Cardy
Discount card storage. Specification: development specification-v2.md.
| Package | Port | Public URL | Proxy auth |
|---|---|---|---|
server-fe — admin UI (Vite) |
10001 | https://dev.aggtaa.com/ |
Authelia |
server-be — admin API |
10002 | https://dev.aggtaa.com/api/admin/* → /admin/* |
Authelia, passes Remote-Email |
server-be — client API |
10002 | https://dev.aggtaa.com/api/* → /* |
none |
mobile-client — PWA |
10003 | https://dev.aggtaa.com/m/* → /* |
none |
The proxy strips /api and /m and passes them in X-Forwarded-Prefix.
Setup
pnpm install
cp server-be/.env.example server-be/.env # fill in KEY_ENCRYPTION_SECRET, SUPERADMIN_EMAIL
cp server-fe/.env.example server-fe/.env
Run
pnpm --filter @cardy/server-be dev # API on 127.0.0.1:10002 (tsx watch)
pnpm --filter @cardy/server-fe dev # admin UI on :10001
pnpm --filter @cardy/mobile-client start # PWA: build + vite preview on :10003 (service worker needs a build)
Production-like: pnpm --filter @cardy/server-be build && pnpm --filter @cardy/server-be start,
pnpm --filter @cardy/server-fe start (build + vite preview on :10001).
The mobile client dev server (pnpm --filter @cardy/mobile-client dev) works locally at
http://localhost:10003/m/; behind the proxy use start.
Android app
pnpm --filter @cardy/mobile-client build:android # web build for Capacitor + cap sync
Then open mobile-client/android in Android Studio (or gradlew assembleDebug there) to build the APK.
The app registers cardy://login?... links (key QR codes) and asks for camera access when scanning.
Live updates (no APK reinstall)
The APK contains the web build and works fully offline. Newer web builds are delivered over the air
(@capgo/capacitor-updater in manual mode, no Capgo cloud, telemetry disabled):
pnpm --filter @cardy/mobile-client publish:android # build + zip into mobile-client/updates/android
The mobile-client server on :10003 serves it at https://dev.aggtaa.com/m/android/version.json.
On launch and on every return to the foreground (when online) the app checks it, downloads a newer
bundle in the background and reloads into it on the card list; on a card or in the editor it waits
(next(): applied on backgrounding). A bundle that fails to start is rolled back automatically.
Versions come from the build time (2026.929.151230 = 2026-09-29 15:12:30 UTC).
APK self-update (native part changed)
Changes to the native part (plugins, AndroidManifest.xml, Java code) need a new APK:
pnpm --filter @cardy/mobile-client publish:android-apk # signed release APK into mobile-client/updates/android
It needs mobile-client/android/keystore.properties (copy keystore.properties.example, not committed)
with the release key. versionCode is the build time (yyDDDHHmm, UTC), versionName the web version.
Installed apps show "A new version of the app is available" on the card list; Install downloads the
APK and hands it to Android's installer (first time: allow "Install unknown apps" for Cardy, then
confirm the system dialog). Web updates published after an APK release require that APK or newer.
All builds (also Android Studio debug builds) are signed with the release key, so they can replace each other. An app signed with another key (e.g. the old debug key) must be uninstalled once.
Live reload during development
pnpm --filter @cardy/mobile-client android:dev (with pnpm --filter @cardy/mobile-client dev running
and a device connected via adb) installs an app that loads the dev server through adb reverse.
Run build:android again before building an APK for real use.
server-be listens on 127.0.0.1 by default because /admin/* trusts Remote-Email.
If the proxy runs on another machine, set HOST and ADMIN_PROXY_SECRET, and make the proxy add
X-Cardy-Proxy-Secret on the /api/admin route.
Without the proxy (local development), server-fe emulates it: forwards /api/* to server-be
with /api stripped; DEV_REMOTE_EMAIL in server-fe/.env fakes the Authelia user.
Tests
pnpm test
pnpm typecheck
Containers and CI (Woodpecker)
docker/*.Dockerfile build the three images from the repository root (docker build -f docker/server-be.Dockerfile .):
| Image | Port | Notes |
|---|---|---|
server-be |
10002 | Node, data in volume /data. Listens on 0.0.0.0: set ADMIN_PROXY_SECRET and don't publish the port publicly |
server-fe |
10001 | nginx with the admin UI build |
mobile-client |
10003 | nginx with the PWA; Android updates are mounted at /updates |
.woodpecker/ci.yaml: typecheck + tests on every push/PR, then images are pushed on main and ci
(tags: short commit SHA and branch name, e.g. :main). Images go to the Gitea registry (git.aggtaa.com/anton/cardy-*); add secrets registry_username
(Gitea user + access token with write:package) and registry_password. Images are built with kaniko, so the repository doesn't need to be Trusted.
docker/compose.example.yml shows how to run the images. APK/OTA publishing stays local (needs the signing key).