An offline-first todo mobile app for Android and iOS, built with NativePHP, Laravel 13, and Livewire Volt.
Companion API repository: https://github.com/numencode/tickabox-api
Tickabox is a native mobile application that lets you manage todos on your device even without an internet connection. Changes made offline are queued locally and automatically synced to the backend API whenever connectivity is restored.
- Offline-first — every action writes to local SQLite immediately; network is never required
- Automatic sync — changes push to the API the moment you come online
- Conflict resolution — last-write-wins by
last_modified_attimestamp; no data is ever silently lost - Secure auth — Sanctum token stored in NativePHP SecureStorage (encrypted on-device)
| Requirement | Version |
|---|---|
| PHP | 8.3 or newer |
| Composer | 2.3 or newer |
| Node.js | 18 or newer |
| Java (JDK) | 17 or newer |
| Android Studio | Latest stable (for emulator / device builds) |
| Android SDK | API level 33 minimum, 36 target |
iOS builds require a Mac with Xcode installed.
| Layer | Technology |
|---|---|
| Framework | Laravel 13 |
| UI | Livewire 4 + Livewire Volt (single Volt component) |
| Native runtime | NativePHP Mobile ~3.1 |
| Local database | SQLite |
| HTTP client | Laravel Http facade |
| CSS | Custom BEM classes (tk-* prefix) |
| Frontend build | Vite |
| Linter | Laravel Pint |
NativePHP plugins:
nativephp/mobile-device— device detection (physical vs. emulator)nativephp/mobile-network— network connectivity statusnativephp/mobile-dialog— native dialog support (installed, not yet used)
This app requires the Tickabox API to be running and reachable for sync to work. The app is fully functional offline without it, but account creation, login, and sync all depend on the API.
API repository: https://github.com/numencode/tickabox-api
git clone https://github.com/numencode/tickabox.git
cd tickabox
cp .env.example .envEdit .env — set TICKABOX_API_URL to your running API instance, then:
composer run setup
npm install && npm run build# Install PHP dependencies
composer install
# Generate application key
php artisan key:generate
# Run database migrations (creates local SQLite database)
php artisan migrate
# Install and build frontend assets
npm install
npm run buildCopy .env.example to .env and configure the values below.
APP_NAME=Tickabox
APP_ENV=local
APP_KEY= # generated by key:generate
APP_DEBUG=false
# Android emulator maps 10.0.2.2 to the host machine's localhost.
# Use your server's HTTPS URL for production builds.
TICKABOX_API_URL=http://10.0.2.2:8000TICKABOX_API_TIMEOUT=30 # seconds for sync requests
TICKABOX_API_AUTH_TIMEOUT=15 # seconds for login/register requests
TICKABOX_API_CONNECT_TIMEOUT=5 # seconds to establish a connectionNATIVEPHP_APP_ID=com.numencode.tickabox # Android/iOS bundle identifierProduction builds must use HTTPS for
TICKABOX_API_URL. The app enforces this at boot time and will refuse to start if a non-HTTPS URL is configured in a non-local environment.
Every todo mutation (create, toggle, delete) follows this sequence:
- Write locally to SQLite immediately — UI updates instantly, no waiting for network
- Queue a
SyncOperationin the local outbox table with statuspending - Trigger sync — if online,
SyncPendingOperationsJobruns synchronously; if offline, the operation stays queued until the next sync
The user never experiences a loading spinner for local operations.
When syncing, the app collects up to 50 pending outbox operations, deduplicates them by UUID (keeping only the most recent per todo), and POSTs them to POST /api/sync/push. The API returns the confirmed server state for each operation, which the app applies locally using last-write-wins conflict resolution.
Failed operations are retried with exponential backoff (capped at 5 minutes). HTTP 429 responses honour the Retry-After header.
After pushing, the app pulls remote changes using a keyset cursor (since timestamp + since_id) to handle pagination correctly even when multiple todos share the same last_modified_at. Up to 20 pages are fetched per sync cycle (1,000 todos per page).
The last successful pull timestamp is stored in sync_meta as last_pulled_at and used as the starting cursor for the next sync.
Last-write-wins by last_modified_at. On both push and pull, if the incoming record's timestamp is older than the local record's, the incoming change is ignored. The server's clock is always authoritative for writes — the server stamps its own now() on every accepted change.
- Login/register POSTs credentials to the API and receives a Sanctum token
- Token is stored in NativePHP SecureStorage (encrypted on-device)
- Token is also stored in local SQLite as a fallback (for when the SecureStorage plugin is unavailable)
- If any sync request receives HTTP 401, all local auth state is cleared and the user is returned to the login screen
The entire app is a single Livewire Volt component (resources/views/livewire/home.blade.php).
- Login / Register toggle
- Email + password fields (name field shown in register mode)
- Inline error messages mapped from API error codes
| Section | Description |
|---|---|
| Header | App logo, user name + email, logout / sign out all devices buttons |
| Network bar | Online/offline status, connection type (Wi-Fi, Cellular, Ethernet, Emulator) |
| Sync bar | Current sync state (offline / syncing / pending / failed / synced), "Sync now" button |
| Sync message | Transient feedback after each todo mutation |
| Composer | Text input + Add button; Enter key submits |
| Task list | Todos sorted incomplete first, newest first; checkbox to toggle, Delete button |
composer run devThis starts the Laravel dev server, queue worker, log watcher, and Vite in parallel.
# First time or after a clean install
php artisan native:install android --force
php artisan native:run android --watch--watch enables hot reload — PHP file changes reflect in the app without a full rebuild.
For faster iteration without a full rebuild:
php artisan native:jump android./vendor/bin/pintcomposer run testphp artisan native:package android --build-type=releaseThis produces a signed release APK ready for distribution.
Requirements before building:
- Keystore file configured in
.env(ANDROID_KEYSTORE_PATH,ANDROID_KEYSTORE_PASSWORD,ANDROID_KEY_ALIAS,ANDROID_KEY_PASSWORD) TICKABOX_API_URLset to your production HTTPS URL in.env
The release build's "Installing Composer dependencies" step may time out after 300 seconds on slower machines. Fix:
- Open
vendor/nativephp/mobile/src/Traits/PreparesBuild.php - Go to line 245
- Change
->timeout(300)to->timeout(3000)
This is a vendor file change — it will be reset on composer update. Re-apply if needed after updating NativePHP.
adb shell
run-as com.numencode.tickaboxcat /data/user/0/com.numencode.tickabox/app_storage/persisted_data/storage/logs/laravel.logadb exec-out run-as com.numencode.tickabox \
cat /data/user/0/com.numencode.tickabox/app_storage/persisted_data/database/database.sqlite \
> database.sqliteYou can then open database.sqlite with any SQLite browser (e.g. DB Browser for SQLite) to inspect local state.
adb uninstall com.numencode.tickabox
php artisan native:install android --force
php artisan native:run android --watchThe app uses SQLite on-device. The schema is defined in database/migrations/.
Stores the local account. A single user record exists after login, with remote_id (server-side ID) and sanctum_token set.
Each todo has a uuid (primary sync key), sync_status (pending / synced / failed), last_modified_at for conflict resolution, and soft-delete support.
The outbox table. Each mutation creates a record here with the operation type (created / updated / deleted) and a JSON payload snapshot. Processed via exponential backoff with the available_at column gating when a failed operation is next eligible for retry.
Key-value store for sync state. Currently stores last_pulled_at — the ISO 8601 timestamp used as the cursor for the next pull.
Configured in config/nativephp.php and .env.
| Setting | Value |
|---|---|
| App ID | com.numencode.tickabox (via NATIVEPHP_APP_ID) |
| Android compile SDK | 36 |
| Android target SDK | 36 |
| Android minimum SDK | 33 (Android 13) |
| Orientation | Portrait only |
| iPad | Disabled |
| Runtime mode | persistent (Laravel boots once, reused across requests) |
| Permissions | network_state, vibrate |
The NumenCode Tickabox app is created by Blaz Orazem.
For inquiries, contact: info@numencode.com
This project is open-sourced software licensed under the MIT license.