Technical architecture briefing

How the backend is actually built

A deeper companion to the shareholder update — this page is for anyone who needs the real technical picture: the two Laravel applications, what Postgres and Redis are each responsible for, why a small Go service was added in mid-2026, and how to change any of it safely. Nothing simplified into diagrams that don't match the code.

Two separate Laravel applications

"The backend" is actually two independent Laravel apps, on two separate servers, each with its own database. They don't share code at runtime — a WordPress site's login never touches the dashboard's database, and vice versa.

api.accessglow.com

Aura-Login-Backend

The core product. Every WordPress plugin install and every mobile app talks to this API for one thing: visual login. Owns sites, the login flow, membership/device records, plan enforcement, and the /super-admin operator panel.

Postgres · Redis · Go (check-status)
app.accessglow.com

aura-dashboard

The customer-facing account dashboard — where a site owner manages their own settings, seats, and billing. Its own database, its own login flow (built on the same visual-login package as a dependency, not a shared runtime).

Postgres

They run on different physical servers today (Helsinki for the API, Nuremberg for the dashboard and a few smaller sites) — not a deliberate geographic strategy, just how the infrastructure grew. Both are single-server deployments: one box running the app, its database, and its cache, with nothing else competing for the same CPU cores.

The core API: Laravel, Postgres, Redis

Aura-Login-Backend runs on Laravel Octane with RoadRunner — not traditional PHP-FPM. The difference matters: PHP-FPM rebuilds the entire framework from scratch on every single request; Octane keeps a fixed pool of already-booted PHP worker processes alive between requests, so a request only pays for its own logic, not for re-bootstrapping Laravel every time. Currently tuned to 24 workers on a 4-vCPU box — that number was arrived at by measurement (12 → 24 → 32 workers, each tested under real load), not guessed.

Postgres — the system of record

Every durable fact lives here: sites, users, memberships, subscriptions, login attempts, and the live state of every in-progress visual login (visual_login_requests). If it needs to still be true after a restart, it's in Postgres.

A production query-profiling pass found two specific queries responsible for ~90% of the database's total query time — one was scanning every pending login system-wide instead of scoping to the site being logged into, the other was missing an index entirely. Fixing both cut total query time roughly13× with no infrastructure change.

Redis — cache, sessions, coordination

Session storage, the application cache, rate-limit counters, and (as of this year) a lightweight pub/subchannel: whenever a login's status changes, a message is published so anything waiting on that specific login can react immediately instead of having to keep asking.

Nothing durable lives only in Redis — losing it would cost active sessions and in-flight rate-limit windows, not historical data.

Why one endpoint is now a small Go service

One specific API call — check-status, the one a WordPress site polls while waiting for someone to confirm a login on their phone — behaves differently from everything else the API does: instead of answering instantly, it deliberatelyholds the connection open for up to ~10 seconds, waiting to see if the status changes, so the caller doesn't have to keep re-asking every couple of seconds.

That's a good trade for anyone calling it from far away — fewer expensive long-distance round trips for the same wait — but it exposes a real structural limit in Octane's worker model:every held request pins one of the 24 fixed workers for the entire hold. With no limit on how many logins can be waiting at once, a burst of concurrent logins could pin every worker in the pool simultaneously — and once that happens, theentire application stops responding to anything else: new logins, the admin panel, everything, until a hold times out.

The first, safer fix was a Redis-backed semaphore capping concurrent holds at 12 — half the worker pool — so the worst case was survivable. That's a real constraint of the runtime, not a bug: Octane's worker model is built around request/response, not holding thousands of connections open.

Go's concurrency model doesn't have that constraint. A goroutine waiting on a timer or a channel costs kilobytes of memory, not an operating-system process — so a Go program can hold tens of thousands of connections open concurrently on hardware where a fixed-worker-pool server can only hold a few dozen. That's not a general "Go is faster" claim — it's a specific, structural fit between the tool and this one workload's shape.

What it actually does

Concurrent long-polls, PHP version12maxartificial cap, protects the worker pool
Concurrent long-polls, Go version500tested, no cap hitverified against production, not a benchmark claim
Peak memory for those 500 holds47.5MBless than a single PHP worker typically uses alone

This was a deliberately narrow bet, not a rewrite. Everything security-sensitive — cryptographic site authentication, biometric signature verification, the actual login state machine — stays in Laravel. The one thing moved to Go was chosen specifically because it's read-mostly, idempotent, and low blast-radius if anything about the migration were ever wrong.

Live in production as of 2026-07-29, confirmed serving realcheck-status traffic on api.accessglow.com — not just deployed and benchmarked in isolation. Re-running the same realistic load test used throughout this section, at the same concurrency, before and after cutover:

Throughput, 500 concurrent loginswas 121.5 / sec155.1/ sec↑ 27.6%, same hardware
p95 latency, same testwas 3.87s3.35s↓ 13.4%
Success rate, both runs100%unchanged — this was a speed win, not a reliability fix

A second, unrelated limit was found and fixed the same week: the application server's own container had a default 1,024 open-file ceiling — invisible under normal load, but a hard wall once concurrent logins approached that count, causing a sharp cliff in error rate rather than a gradual slowdown. Traced to the exact process (RoadRunner's own connection-handling process, not a PHP worker) by watching its live file-descriptor count climb into the 1,000s during a real test, then confirmed fixed by rerunning the identical test after raising the limit to 65,536 — 100% success at the same concurrency that previously failed outright. A reminder that "find the next real bottleneck, fix only that" keeps paying off even after the headline architecture change is done.

How to tweak these connections

Every number in this system is a named, environment-driven config value — nothing load-bearing is hardcoded. The table below is the practical reference for what to change and where, without needing to read the source first.

WhatWhereControls
RoadRunner worker countLaravel API, DockerfileHow many concurrent requests Octane can process at once. More workers ≠ free — each one costs memory and competes for the same CPU cores.
AURA_VISUAL_LOGIN_LONGPOLL_SECONDSLaravel API envHow long check-status holds a connection before giving up and answering with whatever it currently knows.
AURA_VISUAL_LOGIN_LONGPOLL_SLOTSLaravel API envPHP path only — how many concurrent holds are allowed before falling back to an instant response, protecting the worker pool. Not used by the Go service, which has no such cap.
Per-site / per-IP rate limitsLaravel API, config/aura.phpIndependent limits for site registration, login requests, status polling, and finalization — each keyed differently (some by site, some by IP) depending on who's actually making the call.
DATABASE_URL / REDIS_ADDRGo service envWhich Postgres/Redis instance it talks to. Always the same ones the Laravel API uses — this service has no data of its own.
REDIS_CHANNEL_PREFIXGo service envMust exactly match Laravel's own computed Redis key prefix, or the Go service silently stops receiving wake-up messages and falls back to timing out on every call. The single most important value to get right when touching either side.
Postgres role for the Go servicedeploy/postgres-role.sql, Go service repoDefines exactly what it can read and write — read-only on the login table, narrow write access on two usage-tracking columns, nothing else. Changing its permissions is a deliberate, auditable SQL change, not a code change.
The overall design principle: every piece has the smallest access it can function with, every meaningful number is measured rather than assumed, and nothing gets moved to new infrastructure unless there's a specific, verified problem it solves. The Go service exists because one endpoint had a structural limit that couldn't be tuned away — not because Go is generically better than PHP for this application.