Scotty Fermo
Zwipe is a solo project: designed, built, and shipped by one person. This page is the look under the hood, the architecture and the engineering discipline behind a one-person, full-stack Rust app. The goal was simple: make deck building feel good on mobile, and build it that way from the ground up.
System Architecture
Five Rust crates in one workspace. What each one does, and where it pulls from.
zwiper
- Swipe to build, search cards, keep decks in sync
- Talks to zerver over HTTPS
- Imports zwipe-core and zwipe-components
zite
- Marketing, landing, password reset, and email verification
- Talks to zerver over HTTPS
- Imports zwipe-core and zwipe-components
zerver
- The REST API behind everything: auth, sessions, decks, cards, and users
- Reads and writes a PostgreSQL database
- A nightly job pulls the card catalog from Scryfall
- Imports zwipe-core
zwipe-components
- The Dioxus UI shared across the clients: buttons, action bar, card row, changelog
- Reused beyond Zwipe on scottyfermo.com
- Imports zwipe-core
zwipe-core
- Models, filter logic, and traits, with no server- or client-only dependencies
- Imported by every other crate
- Same domain code runs SQL filtering on the server and in-memory filtering on the device
Under the Hood
The engineering discipline behind it.
Hexagonal architecture
Ports and adapters, in practice.
- zwipe-core: zero framework dependencies
- Inbound and outbound adapters swap freely
- Same domain code: server SQL and on-device filtering
- One codebase, compiled to iOS, Android, and web
Testing & lint discipline
694 tests, 406 in zwipe-core. Enforced by the compiler.
.unwrap,panic!,todo!,dbg!, and friends denied at compile time- 22 Clippy rules, workspace-wide
- Compile-time SQL: sqlx
query!fails the build, not runtime - Nightly Cloudflare R2 backups
Authentication
Hand-rolled, stricter than a deckbuilder needs.
- Argon2id, with length, character-class and repetition rules
- Rotating refresh tokens, replay-safe
- Short-lived JWTs; refresh tokens stored hashed
Passwordnewtype consumed on hash, so plaintext can't leak- Rate limiting, audit logs, transactional email
Type safety
Newtypes everywhere. Invalid states don't compile.
UserId,Email,Password: real types, notString- Builders enforce required fields at construction
- Formats as enums and traits, not bool flags
- Validate once at the boundary, trust it downstream
Card data pipeline
110k+ printings nightly from Scryfall. The hard part isn't the cron.
- Five-strategy upsert: batch, then per-row on conflict
- ~327 cards per batch under Postgres's 65k-param cap
PartialEqdelta detection: only changed rows written- Materialized view for dedup search (~35k unique)
- Roles derived at sync, stored on the row: filter one indexed column (see card_role)