Skip to content

numencode/tickabox-api

Repository files navigation

Tickabox API

Backend API for the Tickabox mobile application — built with Laravel 13 and Laravel Sanctum.

Tickabox mobile app: https://github.com/numencode/tickabox


Overview

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_at timestamps
  • 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.


Requirements

Requirement Version
PHP 8.3 or newer
Composer 2.3 or newer
MariaDB 10.6+
MySQL (alternative) 8.0+

Tech Stack

  • 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)

Installation

Quick setup (recommended)

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

Edit .env with your database credentials, then run:

composer run setup

This single command installs dependencies, generates the app key, runs migrations, and seeds any required data.

Manual setup (step by step)

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

Environment Configuration

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

Minimum required

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 authentication)

SANCTUM_TOKEN_EXPIRATION=129600   # token lifetime in minutes (129600 = 90 days)
SANCTUM_TOKEN_PREFIX=tickabox_    # optional prefix on all issued tokens

Production checklist

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=false in production. Leaving it true exposes stack traces and internal application details in API error responses.


Development Commands

# 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/pint

API Reference

All routes are prefixed with /api.

Authentication

POST /api/register

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"
  }
}

POST /api/login

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


POST /api/logout

Revokes the current device's token.

Auth required. No request body.

Response 200 OK:

{ "message": "Logged out." }

POST /api/logout/all

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." }

GET /api/me

Returns the authenticated user's profile.

Auth required.

Response 200 OK:

{
  "user": {
    "id": 1,
    "name": "Jane Smith",
    "email": "jane@example.com"
  }
}

Sync

All sync endpoints require authentication and are rate-limited to 60 requests/min per user.


POST /api/sync/push

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 request
  • operations.*.operation: one of created, updated, deleted
  • operations.*.payload.title: required for created; string, 1–255 characters
  • operations.*.payload.last_modified_at: required for updated; ISO 8601 format (Z or 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.


GET /api/sync/pull

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.


Rate Limiting

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.


Security

  • All authenticated routes require a valid Sanctum Bearer token in the Authorization header
  • Users marked is_active = false are rejected with HTTP 403 on all protected routes
  • Each user's todos are strictly isolated — the server enforces user_id scoping 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

Deployment

Queue worker

This application uses database-backed queues. Run a queue worker on your server:

php artisan queue:work --sleep=3 --tries=3

For production, use a process manager (Supervisor, systemd) to keep the worker running.

Scheduler (cron)

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_tokens table.

Recommended production stack

  • 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=false and APP_ENV=production in .env

Running Tests

Feature tests are written with Pest and cover authentication, sync operations, conflict resolution, and rate limiting.

composer run test
# or
php artisan test

CI runs the full test suite and linter on every push via GitHub Actions.


Related Project

This API is designed to work exclusively with the Tickabox NativePHP mobile application:

Mobile app repository: https://github.com/numencode/tickabox


Author

The NumenCode Tickabox API is created by Blaz Orazem.

For inquiries, contact: info@numencode.com


License

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

About

Backend API for the Tickabox mobile application built with Laravel.

Topics

Resources

Stars

Watchers

Forks

Contributors

Languages