Payky is a local-first point-of-sale application for managing terminal checkout flows, catalog items, bills, payments, account transactions, and background sync jobs.
The app is built with React, TypeScript, Vite, TanStack Router, Tailwind CSS, shadcn-style local UI components, Base UI primitives, Zod validation, and Evolu for persistent application data.
- Bun
For native iOS development, install Xcode and the Xcode Command Line Tools. Capacitor 8 targets iOS 15 and newer.
Install dependencies with exact versions:
bun installDependency versions are pinned through Bun. Keep exact = true in
bunfig.toml.
Install the locked Codex skills from skills-lock.json:
bunx skills experimental_installStart the Vite dev server:
bun run devBuild the app:
bun run buildPreview a production build:
bun run previewCapacitor is available as the Android native target:
bun run cap:android:sync
bun run cap:android:dev
bun run cap:android:build
bun run cap:ios:sync
bun run cap:ios:run
bun run cap:ios:openCapacitor builds use the native HTTP bridge through CapacitorHttp so mobile
requests are not limited by browser CORS behavior.
For Capacitor Android live reload, run:
bun run cap:android:devThe script starts the HTTP Vite dev server, waits for
http://127.0.0.1:5173, forwards the port through Capacitor's Android live
reload flow, and launches the native Android app. The HTTP server is used only
for this debug flow so Android WebView does not reject Vite's self-signed HTTPS
certificate.
For Capacitor iOS simulator development, run:
bun run cap:ios:runFor Xcode-driven iOS development, run:
bun run cap:ios:sync
bun run cap:ios:openConfigure the signing team, bundle identifier, provisioning profile, display
name, icons, launch screen, and deployment target in Xcode. The default bundle
identifier is me.payky.
For iOS live reload on a physical iPhone, start Vite on the local network in one terminal:
PAYKY_DISABLE_BASIC_SSL=1 bun run dev -- --host 0.0.0.0 --strictPortThen point Capacitor at the Mac LAN URL from another terminal:
PAYKY_CAPACITOR_SERVER_URL=http://<mac-lan-ip>:5173 bunx cap run iosThe iPhone and Mac must be on the same network. iOS does not have an adb reverse equivalent, so a physical device cannot use localhost to reach the
Mac dev server.
The Android release build signs with payky-release.keystore. Set these
environment variables before running bun run cap:android:build:
PAYKY_ANDROID_KEYSTORE_PASSWORD=...
PAYKY_ANDROID_KEY_ALIAS=...
PAYKY_ANDROID_KEY_PASSWORD=...Run the full validation suite:
bun run checkRun checks individually:
bun run check:lint
bun run check:ts
bun run check:tests
bun run check:coverageFormat files with Biome:
bun run formatRun Vitest in watch mode:
bun run test:watchsrc/main.tsxis the browser entry point.src/App.tsxwires top-level providers and the TanStack Router provider.src/routescontains file-based route definitions.src/components/uicontains reusable shadcn-style UI primitives built on Base UI.src/i18n/resources.tscontains English and Czech translation resources.src/core/evolucreates the Evolu client and composes the application schema.src/core/modulescontains domain modules for accounts, transactions, catalog items, bills, payments, tables, reconciliation claims, settings, devices, and Fio integration.bincontains CLI commands for local data management and background jobs.
Local UI components live in src/components/ui and should be imported directly
from their owning module:
import { Button } from "@/components/ui/button.tsx"When adding shadcn components, use Bun:
bunx shadcn@latest add buttonKeep generic reusable UI in src/components/ui; feature and domain logic should
live outside that directory.
Persistent application data is stored through Evolu. Register new tables and
indexes in src/core/evolu/schema.ts, and keep domain code in the owning module
under src/core/modules.
Domain modules generally use this structure:
*-types.tsfor branded ids, enums, unions, and exported domain types.- Module root files, such as
payment.ts, for Evolu table schemas and row exports. *-actions.tsfor Evolu mutations and command-style operations.*-queries.tsfor reusable Evolu queries and read models.*-utils.tsfor pure helpers.*.test.tsbeside the module it covers.
Each device account has a 128-bit master secret S (the MasterKey). For a
path P, derive a child key and independent 32-byte entropy:
R = BIP32.MasterKey(S)
K = BIP32.Derive(R, P)
E = HMAC-SHA512(
key = "bip-entropy-from-k",
message = K.privateKey
)[0:32]
Payky paths follow BIP-85
with the BIP-39 application: m/83696968'/39'/{language}'/{words}'/{index}',
language 0' = English. The word count encodes how much of E the consumer
uses (24 words = 32 bytes, 12 words = 16 bytes). The paths must never change
after accounts exist.
| Consumer | Path P |
Result |
|---|---|---|
| Cashu wallet (reserved, unused) | m/83696968'/39'/0'/24'/0' |
index 0' is set aside; nothing derives from it yet |
| Evolu master owner | m/83696968'/39'/0'/24'/1' |
E as the 32-byte Evolu owner secret |
| Default Spark wallet | m/83696968'/39'/0'/12'/0' |
E[0:16] as the 16-byte Spark wallet secret |
The Spark wallet secret is stored as hex and used as BIP-39 entropy: wallet initialization and the settings UI encode it as a 12-word mnemonic (never the raw secret), so the wallet can also be restored in any BIP-39-compatible Spark client. There is no Cashu wallet yet — the path is reserved so that when one ships, its secret won't collide with an index already used by something else.
S itself is backed up as a single SLIP-39
20-word recovery mnemonic (src/core/modules/shared/key-derivation.ts, via
the slip39-ts library), encoded as one group with a 1-of-1 threshold — there
is currently no multi-share Shamir splitting, so the phrase is the sole backup
of S and must be treated with the same care as a BIP-39 seed phrase. SLIP-39
mnemonics use their own wordlist and checksum and are not interchangeable with
BIP-39 mnemonics. The mnemonic's identifier (SLIP-39's 15-bit metadata field)
is derived deterministically from S via HMAC-SHA512 rather than randomized,
so encoding the same S always produces the same recovery phrase. 256-bit
master keys and their 33-word recovery mnemonics are not supported.
The CLI reads .env files automatically. Environment variables are validated at
startup with @t3-oss/env-core and Zod.
PAYKY_SQLITE_PATH=./.data/payky.db bun bin/cli.ts payments list
bun --env-file=.env.cli bin/cli.ts accounts listSupported variables:
PAYKY_SQLITE_PATH: SQLite database file path. Defaults to.data/payky.db.
Current runtime caveat: the CLI uses better-sqlite3, which may fail under Bun
in environments where Bun does not support that native module. If that happens,
the browser app and Vitest checks can still be run normally through the scripts
above.
All user-facing React text should come from src/i18n/resources.ts. Add keys for
both en and cs, and use stable, feature-scoped names such as
checkout.save, settings.items.title, or activity.empty.


