Skip to content
Alex Hollyman

Platforms and APIs

My day-to-day work is building and maintaining the platforms behind apps and devices out in the field: the APIs they talk to, the administration systems that sit behind them and the infrastructure it all runs on. Most of it is in the Laravel ecosystem, hosted on AWS or DigitalOcean.

How I shape an API

I give each kind of caller its own way of signing in, with its own authentication guard: a device registers once and gets a signed token that expires, people sign in to the admin system with a normal session, and any other system we integrate with gets a key of its own for that one integration.

I like every endpoint to behave the same way: JSON in and out, a version in the path, lists that page, and one error shape, a 422 that tells you exactly which field is wrong and why. Anything a caller might reasonably retry is safe to send twice, batches are answered entry by entry with a 207 so one bad entry cannot lose the rest, and the same validation rules run whether a record arrives through the API or through an import.

Request

POST /api/v2/sessionsAuthorization: Bearer eyJhbGciOi…Content-Type: application/json{  "device_id": "dv_7f3a",  "started_at": "2026-10-10T08:14:02Z",  "duration_seconds": 1860  "duration_seconds": -4}

201 Created

{  "id": "ses_8k2m",  "status": "stored"}

422 Unprocessable Content

{  "errors": {    "duration_seconds": [      "must be at least 1"    ]  }}
The same request sent twice: the first is stored, the second has one field wrong and the response says exactly which one.

What a platform is made of

The shape is much the same from one platform to the next: a web server on the edge of a private network, the Laravel application behind it, a database per tenant, a cache, a queue with workers picking jobs off it, object storage sat behind a CDN and an error monitor keeping an eye on the lot.

  • Device Device
  • Phone app Phone
  • Browser (admin) Browser
  • Queue (SQS) Queue
  • Web server (nginx) nginx
  • CDN (CloudFront) CDN
  • Workers Workers
  • Application (Laravel) Laravel
  • Error monitor (Sentry) Sentry
  • Cache Cache
  • Databases, one per tenant Databases
  • Object storage (S3) S3
  • Private network (VPC) VPC

A device signs in

  1. The device calls the handshake endpoint with its registered identifier.
  2. The application looks the device up in the database.
  3. It signs a JSON Web Token and sends it back.
  4. Every later call from the device carries the token.

A session is uploaded

  1. When a session ends, the device posts its results, inputs and event logs. The events arrive as one batch.
  2. The application validates each event on its own. One of them fails.
  3. The events that passed are stored in the tenant’s database.
  4. A job goes on the queue, and a worker picks it up.
  5. The answer is 207 Multi-Status: a list of what was stored, and a list of what failed and why. One bad entry has not lost the rest.

A large file is uploaded

  1. In the admin system, the browser asks the application for somewhere to put a large file.
  2. The application answers with a presigned URL, which allows that one upload for a short time.
  3. The browser sends the file straight to object storage. It never passes through the web server or the application.

A request arrives for one tenant

  1. A request arrives at one tenant’s subdomain.
  2. The application reads the subdomain and works out which tenant it belongs to.
  3. It switches to that tenant’s own database, cache and file storage. The other tenants’ are left alone.
  4. The response goes back, built only from that tenant’s data.

This is a model of the kind of platform I build rather than a map of any one system. Drag the board sideways to turn it.

The administration tools

Alongside every API I build an administration system for the people actually running the platform. It is the same Laravel application and the same database, behind a separate sign-in, with a custom interface built in Vue.js and Inertia rather than an off-the-shelf admin package.

What someone can see and do depends on their role, so an administrator for one organisation manages their own users, devices and content and nothing beyond that. This is where devices get registered (and switched off again when they need to be), where content is uploaded, large video files included, and where everything the devices send back ends up, from completed sessions and results through to the entries that failed validation and why.

It is also where data goes in and out by hand, whether that is importing from a spreadsheet or exporting for reporting, and those go through the same checks as the API does.

Admin Platform admin Organisation admin
  • Dashboard
  • Organisations
  • Users
  • Devices
  • Content
  • Sessions
  • Reports
  • Settings

Devices

dv_7f3a 2 min ago
dv_91c0 14 min ago
dv_2b8e 3 days ago
dv_c44d 1 hr ago

Content

briefing.mp4 412 MB Converted
site-b.glb 96 MB Checked
walkthrough.mp4 1.2 GB Converting

Latest sessions

dv_7f3a just now 31 min
dv_c44d 08:02 27 min
dv_91c0 07:41 12 min
The menu changes with the role, a single device is switched off without touching the rest, and a new session lands in the list as a device finishes.

What it runs on

Hosting
AWS (EC2, RDS, S3, CloudFront, SQS, VPC) and DigitalOcean (Droplets, Spaces, VPC, managed databases)
Servers
Laravel Forge, nginx, Apache
Queues and jobs
Laravel Horizon, Redis, SQS
Edge
Cloudflare, Cloudflare Workers
Delivery
Git, GitHub Actions, Docker
Monitoring
Sentry, Laravel Pulse, Laravel Telescope
Search
Laravel Scout, Meilisearch
Email
Amazon SES, Postmark
A push to main runs the tests, builds and rolls out to staging and then production, whilst a pull request that fails its tests gets no further.