← all projects
personal

Roluna

A swipe-to-discover job-search app for iOS: set the role you want, then swipe through real postings one card at a time. Right swipe saves, left swipe passes, and saved roles get their own tab with apply tracking. Built solo with React Native + Expo on a Node + MongoDB backend, and currently live on TestFlight.

built by
solo
listing refresh
2× a day
status
TestFlight

Overview

Roluna reframes the job hunt as a swipe deck. Users sign in, set a target role with optional filters (location, distance, job type, salary range), and swipe through live job listings rendered as cards: right to save, left to pass. Saved listings live on their own tab where they can be marked as applied, and tapping a saved row opens the original posting on the source site.

Mental model: search results, rendered as a Swipe deck, with persistent Saved and Applied state.

It's a full-stack solo build: a Node/Express backend serving a cache of real job listings, and a React Native iOS client with a hand-built, gesture-driven swipe deck.

The app

Roluna app icon

Production iOS app icon: a warm clay r on the brand gradient.

  • Roluna: Welcome screen
    Welcome
  • Roluna: Discover swipe deck
    Discover
  • Roluna: Filter sheet
    Filters
  • Roluna: Saved tab
    Saved
  • Roluna: Settings
    Settings

High-fidelity UI · Clay theme · designed in-prototype, captured at iOS 402×874

Discover

A search bar opens a full-screen modal with role autocomplete (~180 roles, fuzzy-matched client-side). A filter rail shows every active filter. The centrepiece is a custom swipe deck built on Reanimated gestures, with animated SAVE/PASS stamps that fade in as you drag and a multi-level undo that survives across the session.

Filters

A half-sheet with location autocomplete (~190 US cities), a stepped distance slider, multi-select job-type chips, a min/max salary range with validation, and date-posted controls.

Saved

An animated segmented control switches between All / To apply / Applied. Each row can be marked as applied or opened on the source site, with a remove confirmation.

Settings

Editable defaults for Role, Location, and Job types persisted on-device, plus account management and Apple-compliant account deletion.

Smart defaults

User defaults are stored locally; on a cold start the app auto-seeds Discover from them, and the first manual search of a session takes over.

Cache-first deck

The backend uses a cache-first deck strategy. A scheduled job ingests listings from the Adzuna API into a shared MongoDB collection twice a day; user searches then hit that cache the vast majority of the time, with an on-demand API fallback for long-tail queries. This decouples user activity from the API's daily quota and keeps the whole system comfortably inside a free tier.

Listings to deck · cache first
  1. Adzuna APIsource

    US listings, on a daily quota.

  2. Ingest cron2× a day

    A twice-daily sweep into the cache, under a Redis lock.

  3. Mongo cache14-day TTL

    Shared jobs, cleaned up on a rolling 14-day window.

  4. Express APIbackend

    Deck, swipes and saved roles, with the on-demand fallback for long-tail queries.

  5. Swipe deckclient

    The iOS client, built on Reanimated.

  6. Saved snapshotper user

    A copy of the job taken at save time, which survives the cache purge.

Key design decisions

Cache-first, quota-aware

Twice-daily bulk ingestion populates a shared listings cache, so most user searches never touch the live API; an on-demand fallback handles rare queries. This single decision is what makes a free-tier listings quota viable for an MVP.

Per-user state that survives the purge

Saved listings denormalise a snapshot of the job at save time, so a saved role stays intact even after the rolling 14-day cache cleanup removes the original posting. Dismissed jobs auto-expire via a database TTL index.

Shared validation contract

One set of Zod schemas (job types, search inputs with a salaryMax ≥ salaryMin refinement, swipe payloads) is shared across client and server through a monorepo workspace, so the two sides can't drift out of sync.

Auth bridge

Firebase handles identity; the backend mints its own user record keyed by the Firebase UID and verifies the token on every request, bridging Firebase Auth to server-side data ownership.

Architecture

LayerChoice
MobileReact Native + Expo (managed), TypeScript strict
NavigationExpo Router (file-based)
Server stateReact Query (TanStack)
AuthFirebase Auth: email/password + native Apple & Google Sign In
Gestures / animationreact-native-gesture-handler + react-native-reanimated
BackendNode + Express + TypeScript
DatabaseMongoDB Atlas
Cache / queuesUpstash Redis
ListingsAdzuna API (US)
ValidationZod schemas shared between client and server
Hosting / CIRender (push-to-deploy) + GitHub Actions
DistributionExpo EAS Build → TestFlight

Engineering challenges

HurdleResolution
Dev build launched to a blank white screenAn SDK-version mismatch: a dependency installed for a newer Expo SDK called native APIs that didn't exist in the pinned React Native version. Pinned every native dep to its SDK-matched version and reinstalled clean.
Unable to find a specification for 'EXDevLauncher'In an npm-workspaces monorepo, Expo's iOS autolinker doesn't recurse into nested node_modules. Hoisted the transitive native modules by declaring them explicitly in the leaf workspace.
New searches kept showing stale cardsThe deck only reloaded when empty, so a fresh search left the old cards in place. Tracked the previous query hash in a ref and split the logic into a query-change effect and a top-up effect.
ITMS-90725 rejection at submitApple's iOS 26 SDK gate flipped to enforced mid-submission. Switched the cloud build worker to the latest macOS/Xcode image, a one-line fix that satisfied the gate even on the older Expo SDK.

Stack

  • React Native
  • Expo
  • Expo Router
  • TypeScript
  • React Query
  • Firebase Auth
  • Reanimated
  • Node
  • Express
  • MongoDB
  • Redis
  • Zod
  • Adzuna API
  • Render
  • GitHub Actions
  • EAS Build
  • TestFlight

Status

The backend and mobile app are complete and run end-to-end on a production TestFlight build. Remaining work is the App Store submission itself: privacy policy, listing metadata, screenshots, and review. The project was built and is run entirely on free tiers, with the Apple Developer Program as the only fixed cost.