Linky is a mobile-first PWA for contacts, Nostr messaging, and Lightning/Cashu payments. It is local-first: data is stored in Evolu (SQLite) and syncs between devices.
The repo also contains a separate public website in apps/site/ intended for linky.fit, while the product app remains a distinct deployment on app.linky.fit.
- Nostr (chat, profile, auth-related flows)
- Evolu (local-first DB + sync)
- Cashu + mints (Lightning wallet flow)
- npub.cash (LN address + mint preference sync)
- Login supports either:
nsec, or- one 20-word SLIP-39 share
- With SLIP-39 login:
- Nostr keypair is derived at
m/44'/1237'/0'/0/0 - deterministic Evolu owner lanes are derived for:
- contacts (
contacts-n) - cashu (
cashu-n) - messages (
messages-n) - owner metadata (
ownerMeta)
- contacts (
- Nostr keypair is derived at
- If user pastes custom
nsecduring a SLIP-39 session, app switches to pasted key locally without immediate Evolu restore/write; choosing Derive switches back to seed-derived key.
- Contacts/cashu/messages owner lanes auto-rotate when owner-local write delta reaches:
OWNER_ROTATION_TRIGGER_WRITE_COUNT = 1000
- Per-type rotation cooldown:
OWNER_ROTATION_COOLDOWN_MS = 60000(1 minute)
- Contacts and valid token data migrate forward; messages are pointer-rotated (no message copy).
- App reads active + previous message owner for continuity.
- Stale owners are pruned locally (
n-2) after rotation. - Contact cap:
MAX_CONTACTS_PER_OWNER = 500
- Contacts: add/edit/delete, QR scan/share, grouping
- Messages: encrypted private chat (gift-wrap/NIP-17 flows)
- Wallet: Cashu token ingest, restore, validation, spend
- Payments:
- Lightning invoice and LN address payment
- contact payment via Cashu message flow
- Push: optional Bun push service in
apps/push/for generic Web Push notifications on new outer inboxkind: 1059events - Debug pages for Evolu current/history data and owner/rotation diagnostics
Requirements: Bun; Docker for the local dev service stack
For Android native builds: Java 17
bun run dev— full local environment: startsdocker-compose.dev.yml(local Nostr relay :7777, Evolu sync relay :4001, Cashu Nutshell FakeWallet mint :3338 that auto-settles invoices with fake sats), then runs the web app (:5173) and push service (:8787) against it via the committed.env.developmentfiles. npub.cash flows are disabled locally (#219); the mint has no real Lightning backend (#220).bun run dev:prod— web app only, on :5175, against production services. The separate port keeps browser storage isolated from local-dev sessions.bun run dev:services— just the docker stack, attached.
Android shell currently adds:
- encrypted native secret storage for identity data
- native QR scanning in the Capacitor shell
- native Android notification permission + FCM token bridge
Native push delivery now works end-to-end when:
apps/native-shell/android/app/google-services.jsonis present for the Android shell buildapps/pushis configured withPUSH_FIREBASE_SERVICE_ACCOUNT_JSON
bun install
bun run dev
bun run site:dev
bun run push:dev
bun run native:android:add
bun run native:apk:debug
bun run native:apk:releaseBuild:
bun run build
bun run site:buildAndroid native shell debug APK:
bun run native:android:add
bun run native:apk:debugAndroid signed release APK:
bun run native:apk:releaseLatest built debug APK ends up at:
apps/native-shell/android/app/build/outputs/apk/debug/app-debug.apkPublic download URL for the latest GitHub Release APK:
https://github.com/hynek-jina/linky/releases/latest/download/linky.apkStart the push service once:
bun run push:startUnit tests (Vitest) across all workspaces:
bun run testEnd-to-end tests (Playwright) live in apps/web-app/tests/*.spec.ts and are split into two
projects. prod-services is the original suite and runs against production relays and mints:
cd apps/web-app && bunx playwright test --project=prod-serviceslocal-stack runs the proxy-payment flow — three accounts on one machine, talking over the local
Nostr relay and paying each other with the local Cashu mint. It needs the docker stack up first,
because the app is served from it as a production build on :5176:
docker compose -f docker-compose.dev.yml --profile e2e up -d --build --wait
cd apps/web-app && bunx playwright test --project=local-stackRe-run the up --build after changing app source; the endpoints are baked into the image.
To watch or debug a run:
bunx playwright test --project=local-stack --ui # step through it
bunx playwright test --project=local-stack --headed # three live browsers
bunx playwright show-trace test-results/*local-stack/trace.zip # after the factEvery run records a trace containing all three accounts, and the console output of each app is
printed prefixed with its account label ([A], [B], [C]). The run takes ~20s, so --ui and the
trace viewer are far more useful than watching it live.
In CI, local-stack gates every release: it runs on each push to main (Vercel Deployment Checks
holds the production promotion until it passes) and as a required job in both Android release
workflows.
Always run the full check pipeline after changes:
bun run check-codeThis runs:
typecheckeslint --fixprettier --write
Workspace-scoped commands (web app only):
bun run --filter @linky/web-app typecheck
bun run --filter @linky/web-app eslint
bun run --filter @linky/web-app prettierWorkspace-scoped commands (public site only):
bun run --filter @linky/site dev
bun run --filter @linky/site build
bun run --filter @linky/site previewWorkspace-scoped commands (native shell):
bun run --filter @linky/native-shell android:sync
bun run --filter @linky/native-shell android:open
bun run --filter @linky/native-shell android:apk:debugPush service workspace commands:
bun run --filter @linky/push typecheck
bun run --filter @linky/push startPush service container artifacts live in apps/push/:
Dockerfilebuilds a production Bun imagedocker-compose.example.ymlshows a persistent SQLite/datavolume for prod-style deployment.env.production.examplelists the runtime env vars expected by that compose setup
Linky is released under the Zero-Clause BSD license (0BSD). See
LICENSE.