Under the glow

Modern Android,
free-tier first.

Wisp is built with Kotlin, Jetpack Compose, MVVM, Hilt, Room, and Firebase — no premium services, no paid SDKs, no compromises on architecture.

Stack

Twelve libraries.
Zero compromises.

Every dependency on this list is mature, free, and well-supported. The whole app builds and runs on the Firebase Spark plan.

KotlinPrimary language
Jetpack ComposeDeclarative UI
Compose NavigationRouting
HiltDependency injection
RoomLocal database
Firebase AuthIdentity
FirestoreRealtime cloud DB
Firebase FCMPush notifications
CameraXCamera capture
Imgur APIImage hosting
CoilImage loading
WorkManagerBackground tasks
JUnit + Compose UITesting
KoverCoverage tracking
Architecture

MVVM, plus a clean
repository seam.

ViewModels expose state, repositories abstract data sources, and the UI is pure Compose. The Imgur + Firestore split is hidden inside MediaRepo and StoryRepo.

Jetpack Compose UI
ChatVM
StoryVM
CameraVM
AuthVM
ChatRepo
StoryRepo
MediaRepo
UserRepo
Room (offline cache)
Firestore
Imgur API
Firebase Auth
Package layout

Where everything lives.

com.example.wisp/
├── di/ → Hilt modules
├── data/
│   ├── local/ → Room DB
│   ├── remote/ → Firestore + Imgur
│   └── repository/ → Repo abstractions
├── ui/
│   ├── auth/ → Login/Register
│   ├── chat/ → List + Detail
│   ├── camera/ → Capture + Preview
│   ├── stories/ → Feed + Viewer
│   ├── profile/ → Profile + AddFriend
│   └── components/ → Shared composables
├── viewmodel/ → AuthVM, ChatVM, ...
├── service/ → FCM + WorkManager
└── util/ → Extensions, constants
Why these splits

Repositories carry the weight.

The repository layer is the seam where the cloud meets the cache. StoryRepo, for example, handles Imgur uploads, writes the story document to Firestore, mirrors it to Room, and exposes a single Flow<List<Story>> to the ViewModel.

UI never touches Firestore directly. ViewModels never touch Room directly. This makes testing trivial — you fake the repository and verify the rest in pure Kotlin.

Data flow

One wisp, end to end.

Follow a single photo from shutter to a friend's screen.

01

CameraX captures

UseCase pipeline writes a JPEG to a temp file.

02

Compress to ~200KB

Quality-aware encoder targets a tight size without crushing the dark areas.

03

POST to Imgur

Anonymous upload with Client-ID header. Returns a public CDN URL.

04

Write Firestore doc

{userId, imageUrl, createdAt, expiresAt} — and friends see it instantly.

val response = imgurApi.upload(image)
val story = Story("abc123", response.data.link, now, now + 24.hours)
firestore.collection("stories").add(story)
storyDao.insert(story.toEntity())
# friends' viewmodels emit immediately via Firestore listeners
Testing

30%+ coverage,
tracked with Kover.

JUnit unit tests cover repositories and viewmodels. Compose UI tests verify navigation and key flows. Kover generates an HTML report on every CI run.

  • Repository tests with fake Firestore + in-memory Room
  • ViewModel tests verifying state transitions on each user action
  • Compose UI tests exercising navigation and chat-send flow
./gradlew test
> 142 tests passed in 18.4s
./gradlew koverHtmlReport
> coverage: 33.7%
./gradlew connectedAndroidTest
> 11 instrumented tests passed
Hard constraints

Free-tier engineering.

Wisp ships entirely on free services. The constraints shaped the architecture.

No Firebase Storage

Storage requires the Blaze (paid) plan. Solution: images go to Imgur, URLs are stored in Firestore documents.

50K reads / day

Firestore Spark plan caps. Solution: Room caches everything, listeners stream incrementally.

No paid analytics

No Crashlytics, Mixpanel, or PostHog. Solution: good logging, manual QA, and Kover for confidence.

All the bits

Build it yourself.

Wisp is open source. Clone the repo, drop in your Firebase config and Imgur Client-ID, and you're running.