About
Zwipe
Zwipe is built and maintained by Scotty Fermo, a Commander player who wanted deck building to work well on a phone. The rest of this page covers how it's built.
System architecture
Six Rust crates in one workspace, on top of orangui, the UI kit I built and share between this app, this site and two other apps of mine. What each one does, and where it pulls from.
Architecture
How it fits together
Mobile app
zwiper
- Swipe to build, search cards, keep decks in sync
- Talks to zerver over HTTPS
- Imports zwipe-core, zwipe-client and components
Website
zite
- Landing and guides, the changelog, shared deck pages, password reset and email verification
- Talks to zerver over HTTPS
- Imports zwipe-core, zwipe-client and components
Backend
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
Shared UI
components
- The Magic UI shared across the clients: card row, deck charts, changelog, this diagram
- Built on orangui, the UI kit I wrote for all my apps: themes, panels, nav, charts
- Reused beyond Zwipe on scottyfermo.com
- Imports zwipe-core and orangui
Shared API client
zwipe-client
- Every API call the app and the site make, described once and sent by one function
- Each endpoint declares its method, path and response type in zwipe-core, so a client cannot call it the wrong way
- No UI framework and no platform code, so it compiles for phones and the browser alike
Shared domain
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
750+ tests, 450+ in zwipe-core.
.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
How AI is used
I learn the approach before a model writes any code.
- It starts with research: teaching myself, cross-referencing with a model, learning the approaches before picking one
- Security checked at every stop, implementations validated against other models, everything tested
- Only once I could build it myself does the model write it, and I read every line
- All of it is open source. Read it; criticism is welcome
Authentication
Built in-house rather than from an auth service.
- 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
User input becomes a validated type before anything trusts it.
Username,DeckName,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
115k+ printings synced nightly from Scryfall.
- 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)