Exequ-Jobs
Offline-first Android app for field technicians: work orders, photos, signatures and time tracking that keep working with zero signal, then reconcile cleanly.
- role
- Designed & shipped · AI-assisted
- status
- live
- date
- 2025 — present
- links
- live ↗
- Kotlin
- Jetpack Compose
- Room
- SQLCipher
- Apollo GraphQL
- Hilt
- WorkManager
- Firebase
[ phone ]──✗──( signal )
│ write
▼
( room db )──▶[ queue ]
│ sync
▼
( cloud ✓ )- lines of code
- 66k
- files
- 247
- shipping
- v1.4+
The constraint that shaped everything
Field technicians work in basements, on roofs, and in parts of South Africa where mobile coverage simply disappears. An app that needs the network to save a job card is useless there. So Exequ-Jobs is offline-first as an architecture, not a feature: the encrypted local database is the source of truth, and the cloud is something the app synchronizes with when it can.
I design and ship this app — AI-assisted, with Claude Code doing a lot of the typing while I own the architecture, review the diffs, and answer for what reaches technicians' phones.
How the sync engine works
I wrote up the architecture long-form in Offline-first isn't a feature, it's an architecture; the short version: every write succeeds locally and enqueues a pending operation. Background workers drain the queue whenever connectivity is healthy:
- SyncOrchestratorWorker drains queued mutations, downloads assigned jobs, and reconciles cloud state in a three-phase merge transaction.
- PhotoSyncWorker uploads job photos and receipts through presigned S3 URLs — and heals orphaned rows if an upload died halfway.
- AutoSaveWorker debounces form state into the store so a technician can be interrupted at any moment.
Real-time updates flow the other way over server-sent events, fanning job and status changes into the local database and the job's participant timeline.
Multi-tenant, one binary
Login goes through Exequtech's central console, which issues short-lived tenant JWTs. A single OkHttp interceptor then retargets every Apollo and REST request to the selected tenant at runtime — no client rebuilds, no app restarts:
@Singleton
class TenantUrlInterceptor @Inject constructor(
private val cloudTokenManager: CloudTokenManager
) : Interceptor {
override fun intercept(chain: Interceptor.Chain): Response {
val originalRequest = chain.request()
val tenantUrl = cloudTokenManager.getTenantUrl() ?: BuildConfig.API_BASE_URL
val targetUrl = tenantUrl.toHttpUrlOrNull()
?: return chain.proceed(originalRequest)
val newUrl = originalRequest.url.newBuilder()
.scheme(targetUrl.scheme).host(targetUrl.host).port(targetUrl.port)
.build()
return chain.proceed(originalRequest.newBuilder().url(newUrl).build())
}
}Details that matter in production
- Encrypted at rest — Room over SQLCipher, with
exportSchemaon and schema JSON committed, so every migration is reviewable in a pull request and covered by instrumentation tests. - Native crash symbolication — Crashlytics with NDK symbol upload wired into the Gradle build, so native stack traces arrive readable.
- ML Kit barcode/QR scanning for stock movements, biometric + PIN auth, UUIDv7 identifiers throughout, and staging/production flavors pointed at separate backends.
Ownership, honestly stated
Shipped continuously since late 2025, currently v1.4+. I'm the only committer — though a substantial share of the keystrokes belong to an AI pair programmer working under my review; the architecture, the decisions, the releases and the accountability are mine.