Backend API for the Tickabox mobile application — built with Laravel 13 and Laravel Sanctum.
Tickabox mobile app: https://github.com/numencode/tickabox
Tickabox API is the central server for the Tickabox offline-first todo mobile app. It handles:
- User registration and login via Laravel Sanctum tokens
- Bi-directional todo sync between mobile devices and the server
- Last-write-wins conflict resolution using
last_modified_attimestamps - Per-user data isolation — no user can access another user's data
- Rate-limited authentication and sync endpoints
The mobile app stores todos locally in SQLite and syncs with this API when connectivity is available. The API persists all data in MariaDB / MySQL.
| Requirement | Version |
|---|---|
| PHP | 8.3 or newer |
| Composer | 2.3 or newer |
| MariaDB | 10.6+ |
| MySQL (alternative) | 8.0+ |
- Laravel 13
- Laravel Sanctum 4.3 — API token authentication
- MariaDB / MySQL — central database
- Pest — feature test suite
- Laravel Pint — code linter
- GitHub Actions — CI (tests + linter)
git clone https://github.com/numencode/tickabox-api.git
cd tickabox-api
cp .env.example .envEdit .env with your database credentials, then run:
composer run setupThis single command installs dependencies, generates the app key, runs migrations, and seeds any required data.
# 1. Install PHP dependencies
composer install
# 2. Generate application key
php artisan key:generate
# 3. Configure your database in .env, then run migrations
php artisan migrate
# 4. Start the development server
php artisan serveCopy .env.example to .env and configure the values below.
APP_NAME=Tickabox API
APP_ENV=local
APP_KEY= # generated by key:generate
APP_DEBUG=false
APP_URL=http://localhost
DB_CONNECTION=mysql
DB_HOST=127.0.0.1
DB_PORT=3306
DB_DATABASE=tickabox_api
DB_USERNAME=root
DB_PASSWORD=SANCTUM_TOKEN_EXPIRATION=129600 # token lifetime in minutes (129600 = 90 days)
SANCTUM_TOKEN_PREFIX=tickabox_ # optional prefix on all issued tokens| Setting | Production value |
|---|---|
APP_ENV |
production |
APP_DEBUG |
false |
APP_URL |
HTTPS URL of your server |
DB_PASSWORD |
strong password |
SANCTUM_TOKEN_EXPIRATION |
adjust to your security policy |
Always set
APP_DEBUG=falsein production. Leaving ittrueexposes stack traces and internal application details in API error responses.
# Quick full setup from scratch
composer run setup
# Start development server
php artisan serve
# Run the full test suite
composer run test
# or
php artisan test
# Lint with Laravel Pint
./vendor/bin/pintAll routes are prefixed with /api.
Creates a new user account and returns a Sanctum token.
Rate limit: 10 requests/min per email+IP, 20 requests/min per IP.
Request body:
{
"name": "Jane Smith",
"email": "jane@example.com",
"password": "SecurePass1"
}Password requirements: minimum 8 characters, mixed case, at least one number.
Response 201 Created:
{
"token": "tickabox_abc123...",
"expires_at": "2026-07-26T10:00:00+00:00",
"user": {
"id": 1,
"name": "Jane Smith",
"email": "jane@example.com"
}
}Authenticates an existing user. Invalidates all previous tokens and issues a new one.
Rate limit: 10 requests/min per email+IP, 20 requests/min per IP.
Request body:
{
"email": "jane@example.com",
"password": "SecurePass1"
}Response 200 OK: Same shape as /api/register.
Response 422 Unprocessable Entity: Returned for invalid credentials (generic message to prevent user enumeration).
Revokes the current device's token.
Auth required. No request body.
Response 200 OK:
{ "message": "Logged out." }Revokes all tokens for the authenticated user (signs out all devices).
Auth required. No request body.
Response 200 OK:
{ "message": "Signed out from all devices." }Returns the authenticated user's profile.
Auth required.
Response 200 OK:
{
"user": {
"id": 1,
"name": "Jane Smith",
"email": "jane@example.com"
}
}All sync endpoints require authentication and are rate-limited to 60 requests/min per user.
Sends local todo changes from the mobile app to the server.
Request body:
{
"operations": [
{
"uuid": "550e8400-e29b-41d4-a716-446655440000",
"operation": "created",
"payload": {
"title": "Buy groceries",
"is_completed": false,
"last_modified_at": "2026-04-27T10:00:00Z"
}
},
{
"uuid": "660e8400-e29b-41d4-a716-446655440001",
"operation": "updated",
"payload": {
"is_completed": true,
"last_modified_at": "2026-04-27T10:05:00Z"
}
},
{
"uuid": "770e8400-e29b-41d4-a716-446655440002",
"operation": "deleted",
"payload": {
"last_modified_at": "2026-04-27T10:10:00Z",
"deleted_at": "2026-04-27T10:10:00Z"
}
}
]
}Validation rules:
operations: array, maximum 100 items per requestoperations.*.operation: one ofcreated,updated,deletedoperations.*.payload.title: required forcreated; string, 1–255 charactersoperations.*.payload.last_modified_at: required forupdated; ISO 8601 format (Zor timezone offset), must not be in the future
Response 200 OK:
{
"results": [
{
"uuid": "550e8400-e29b-41d4-a716-446655440000",
"status": "ok",
"title": "Buy groceries",
"is_completed": false,
"last_modified_at": "2026-04-27T10:00:01+00:00",
"deleted_at": null
}
],
"server_time": "2026-04-27T10:00:01+00:00"
}Each result entry has "status": "ok" on success or "status": "error" with a "message" if that individual operation failed. Other operations in the same batch are not affected.
Conflict resolution (Last-Write-Wins):
The server compares the incoming last_modified_at against the stored record's last_modified_at. If the incoming timestamp is older, the server rejects the change and returns the current server state so the client can reconcile. If equal or newer, the server applies the change. The server always uses its own clock (server_time) as the authoritative write timestamp.
Returns todos that have been modified on the server since the last sync.
Query parameters:
| Parameter | Type | Required | Description |
|---|---|---|---|
since |
ISO 8601 string | No | Return only todos modified after this timestamp. Omit for a full sync. |
since_id |
integer | No | Secondary cursor for paginating within the same since timestamp. Default 0. |
Response 200 OK:
{
"todos": [
{
"uuid": "550e8400-e29b-41d4-a716-446655440000",
"title": "Buy groceries",
"is_completed": false,
"last_modified_at": "2026-04-27T10:00:01+00:00",
"deleted_at": null
}
],
"has_more": false,
"next_since": null,
"next_since_id": null,
"server_time": "2026-04-27T10:00:05+00:00"
}Pagination:
The pull endpoint returns up to 1,000 todos per page. When has_more is true, use next_since and next_since_id from the response as the parameters for the next request. Continue until has_more is false. Store server_time from the final page as the since value for the next sync session.
Soft-deleted todos are included in pull responses with a non-null deleted_at. The mobile app must delete them locally when it receives them.
| Route group | Limit |
|---|---|
POST /api/register, POST /api/login |
10 req/min per email+IP; 20 req/min per IP |
| All authenticated routes (sync, logout, me) | 60 req/min per authenticated user |
Requests that exceed the limit receive HTTP 429 Too Many Requests with a Retry-After header.
- All authenticated routes require a valid Sanctum Bearer token in the
Authorizationheader - Users marked
is_active = falseare rejected withHTTP 403on all protected routes - Each user's todos are strictly isolated — the server enforces
user_idscoping on every query - Tokens are rotated on every login (previous tokens are revoked)
- Token lifetime is configurable via
SANCTUM_TOKEN_EXPIRATION(default 90 days) - Auth endpoints use timing-safe credential validation to prevent user enumeration
- All production deployments must use HTTPS
This application uses database-backed queues. Run a queue worker on your server:
php artisan queue:work --sleep=3 --tries=3For production, use a process manager (Supervisor, systemd) to keep the worker running.
The Laravel scheduler must run every minute. Add this to your server's crontab:
* * * * * cd /path/to/tickabox-api && php artisan schedule:run >> /dev/null 2>&1
Scheduled tasks:
| Task | Frequency | Purpose |
|---|---|---|
sanctum:prune-expired --hours=2160 |
Daily | Remove expired Sanctum tokens (older than 90 days) |
Without this cron, expired tokens accumulate indefinitely in the
personal_access_tokenstable.
- PHP-FPM + Nginx or Apache
- MariaDB 10.6+ with a dedicated database user (least-privilege)
- Supervisor for the queue worker
- SSL certificate (Let's Encrypt or equivalent)
APP_DEBUG=falseandAPP_ENV=productionin.env
Feature tests are written with Pest and cover authentication, sync operations, conflict resolution, and rate limiting.
composer run test
# or
php artisan testCI runs the full test suite and linter on every push via GitHub Actions.
This API is designed to work exclusively with the Tickabox NativePHP mobile application:
Mobile app repository: https://github.com/numencode/tickabox
The NumenCode Tickabox API is created by Blaz Orazem.
For inquiries, contact: info@numencode.com
This project is open-sourced software licensed under the MIT license.