Skip to content

numencode/tickabox

Repository files navigation

Tickabox

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


Overview

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_at timestamp; no data is ever silently lost
  • Secure auth — Sanctum token stored in NativePHP SecureStorage (encrypted on-device)

Requirements

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.


Tech Stack

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 status
  • nativephp/mobile-dialog — native dialog support (installed, not yet used)

Related Project

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


Installation

Quick setup

git clone https://github.com/numencode/tickabox.git
cd tickabox
cp .env.example .env

Edit .env — set TICKABOX_API_URL to your running API instance, then:

composer run setup
npm install && npm run build

Manual setup

# 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 build

Environment Configuration

Copy .env.example to .env and configure the values below.

Minimum required

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:8000

API connection tuning (optional)

TICKABOX_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 connection

NativePHP

NATIVEPHP_APP_ID=com.numencode.tickabox   # Android/iOS bundle identifier

Production 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.


Architecture

Offline-first model

Every todo mutation (create, toggle, delete) follows this sequence:

  1. Write locally to SQLite immediately — UI updates instantly, no waiting for network
  2. Queue a SyncOperation in the local outbox table with status pending
  3. Trigger sync — if online, SyncPendingOperationsJob runs synchronously; if offline, the operation stays queued until the next sync

The user never experiences a loading spinner for local operations.

Sync — Push

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.

Sync — Pull

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.

Conflict resolution

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.

Authentication

  1. Login/register POSTs credentials to the API and receives a Sanctum token
  2. Token is stored in NativePHP SecureStorage (encrypted on-device)
  3. Token is also stored in local SQLite as a fallback (for when the SecureStorage plugin is unavailable)
  4. If any sync request receives HTTP 401, all local auth state is cleared and the user is returned to the login screen

UI

The entire app is a single Livewire Volt component (resources/views/livewire/home.blade.php).

Unauthenticated view

  • Login / Register toggle
  • Email + password fields (name field shown in register mode)
  • Inline error messages mapped from API error codes

Authenticated view

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

Development

Start the development server

composer run dev

This starts the Laravel dev server, queue worker, log watcher, and Vite in parallel.

Run on Android emulator

# 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.

Hot reload via Jump server

For faster iteration without a full rebuild:

php artisan native:jump android

Lint

./vendor/bin/pint

Tests

composer run test

Building for Release

Android APK

php artisan native:package android --build-type=release

This 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_URL set to your production HTTPS URL in .env

Known build issue — Composer timeout

The release build's "Installing Composer dependencies" step may time out after 300 seconds on slower machines. Fix:

  1. Open vendor/nativephp/mobile/src/Traits/PreparesBuild.php
  2. Go to line 245
  3. 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 Debugging

Enter the app sandbox on the emulator

adb shell
run-as com.numencode.tickabox

Read Laravel logs

cat /data/user/0/com.numencode.tickabox/app_storage/persisted_data/storage/logs/laravel.log

Extract the local SQLite database

adb exec-out run-as com.numencode.tickabox \
  cat /data/user/0/com.numencode.tickabox/app_storage/persisted_data/database/database.sqlite \
  > database.sqlite

You can then open database.sqlite with any SQLite browser (e.g. DB Browser for SQLite) to inspect local state.

Reset the app completely (uninstall + reinstall)

adb uninstall com.numencode.tickabox
php artisan native:install android --force
php artisan native:run android --watch

Local Database Schema

The app uses SQLite on-device. The schema is defined in database/migrations/.

users

Stores the local account. A single user record exists after login, with remote_id (server-side ID) and sanctum_token set.

todos

Each todo has a uuid (primary sync key), sync_status (pending / synced / failed), last_modified_at for conflict resolution, and soft-delete support.

sync_operations

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.

sync_meta

Key-value store for sync state. Currently stores last_pulled_at — the ISO 8601 timestamp used as the cursor for the next pull.


NativePHP Configuration

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

Author

The NumenCode Tickabox app is created by Blaz Orazem.

For inquiries, contact: info@numencode.com


License

This project is open-sourced software licensed under the MIT license.

About

Offline-first mobile todo app built with NativePHP and Laravel.

Topics

Resources

Stars

Watchers

Forks

Contributors