Virya Signal began as a mobile control panel for the band. Staff needed tickets, QR scanning, campaigns and the few operational actions that are painful to perform from a laptop at the entrance to a concert.
Then the fan side started to make just as much sense: confirmation, nearby shows, tickets, rewards and the private wallet already existed in CrowdRelay. Putting them into a small mobile application was useful. Pretending that fans and staff were the same kind of user was not.
One shell, two identities
The application therefore has one shell and two trust boundaries.
Fan identity begins with confirmation
A fan enters through email confirmation. They provide an address, city, consent and a local PIN. CrowdRelay sends a one-time confirmation token. The application may receive the token directly, extract it from the complete confirmation URL or scan it from a QR code. After confirmation, the backend creates a fan session and the application stores the credential in the encrypted local vault.
The local PIN protects the device copy. It is not sent to CrowdRelay and it is not another account password pretending to be global identity. Losing the application state should not change the fan record on the server; it should require a controlled session recovery flow.
Staff is a separate security boundary
Staff access is different. A trusted operator device is paired through a separate route and receives a role-scoped credential. Scanning an admission pass does not require the same permission as issuing one. Creating campaigns does not imply access to accounting. An owner, an operator and a person helping at the door may use the same application, but the backend still decides which action each session may perform.
That distinction matters more than the visual mode picker. A universal session with a role field in local storage would be convenient until a fan-side bug, a copied backup or an over-broad endpoint crossed the boundary. Separate vault records, commands, API clients and permission checks make the mistake harder to express.
Make confirmation redundant on purpose
The awkward part was onboarding on a phone. A beautiful confirmation link is still terrible when the application asks somebody to select sixty-four characters from the middle of it. The useful flow became deliberately redundant:
tap the linkor scan the QRor copy the visible tokenor paste the complete URLThe backend accepts the exact token or a confirmation URL containing it. The application normalises the input before sending it and gives the user a specific answer when a token is malformed, expired or already used. Convenience belongs at the edge; token validity still belongs to CrowdRelay.
Mobile failures should stay visible
Mobile failures needed the same honesty. A WebView can disappear, the camera permission may be denied, a native command may time out and Android may expose the page a moment before the Tauri bridge is ready. I did not want those cases to look like a silent return to the home screen.
Virya Signal keeps bounded native calls, visible runtime diagnostics and a crash report that can be copied instead of guessed. The Rust code avoids unwrap and expect in application paths, not because panics can be legislated away, but because input, storage and network failures deserve explicit states.
Loading ends when loading ends
The splash screen follows the same rule. It shows the actual Signal animation while the application starts and disappears as soon as the interface is ready. There is no artificial minimum duration added to make the logo feel important. Loading is a state, not a marketing video.
Share the shell, not the authority
Sharing one application still saves real work. The design language, API transport, QR scanner, error handling and release process are common. The identities are not.
One binary can serve both sides of a concert. The boundary should remain visible even when the code lives in the same repository.