Devlog · Phase 0 · 5/7
The Module Architecture of the Web Repo
Five modules, a single exit point toward the cloud, and reviews you can read from the file path: the Laravel architecture of the web repo, over ninety percent generated by AI agents.
- laravel
- architecture
- octane

A database-free marketing site is a few pages and a form. The temptation, on such a small scope, is to pile everything into two or three controllers and move on. I took the opposite path from the first week of the project. When I put the landing page up in April, I filed each responsibility into its own module. Today the repo has five domains: Cloud, Locale, Seo, Waitlist, and now Blog. Here is what makes that discipline hold, even at this scale.
One Folder Per Domain, Not Per Technical Type
Laravel's starting structure files code by nature: all the controllers together, all the services together, all the utility classes together. On aubia.dev, I switched to filing by business domain. Each folder under app/Modules/ carries a complete product intent, with its Actions/, DataTransferObjects/, Http/, and Enums/ subfolders where they're useful.
Cloud: the single HTTP exit point towardapi.aubia.cloud.Locale: language resolution before routing.Seo: sitemap generation and indexing signals.Waitlist: the double opt-in signup proxy.Blog: the flat-file engine that serves this article.
When an intent changes, I know exactly where to look. No file plays both sides.
Actions: One Intent, One Class
Every business operation lives in its own Action class. No catch-all service piling up fifteen methods over the months: one class does one thing, exposes it through an entry method, and is tested outside the HTTP stack. The Aubia convention wants these classes final readonly by default, with a named entry point (execute) or an invokable one.
Locale resolution is a good example. It replays the same cascade as the middleware, without touching the URL segment, isolated to stay testable:
public function __invoke(Request $request): string
{
$candidates = [
$request->cookie(SetLocale::COOKIE_NAME),
$this->extractAcceptLanguage($request),
config('app.locale'),
];
foreach ($candidates as $candidate) {
if (is_string($candidate) && in_array($candidate, SetLocale::SUPPORTED_LOCALES, true)) {
return $candidate;
}
}
return SetLocale::DEFAULT_LOCALE;
}
The controller that calls this Action stays trivial: it delegates and redirects. All the testable logic is elsewhere.
Readonly DTOs: a Typed Contract Between the Layers
Between one layer and the next, I never pass a loosely typed associative array. Every structured payload goes through a Data Transfer Object in pure PHP: a readonly class with promoted properties, a fromArray() on the way in and an explicit output method. No external dependency for that, no magic mapping: PHP 8.5's strict typing is enough, and the contract reads at a glance.
The waitlist signup shows why this contract matters. The form captures a time-based honeypot field, startedAt, used only to trap bots on the validation side. This field must never go out to the cloud. The DTO carries it internally but excludes it from the outbound payload:
public function toCloudPayload(): array
{
return [
'email' => $this->email,
'utm_source' => $this->utmSource,
'utm_medium' => $this->utmMedium,
'utm_campaign' => $this->utmCampaign,
'locale' => $this->locale,
];
}
The boundary between what the web knows and what the cloud receives is written in black and white, in a method whose name says exactly what it does.
A Single Exit Point Toward the Cloud
Since Phase 0 stores nothing locally, all persistent data goes out to the cloud repo through an HTTP proxy. I centralized these calls in a single class, CloudApiClient, so the Actions never have to know the details of Laravel's native Http client: timeouts, backoff, logging with no personal data. The client applies two distinct retry policies based on the verb's idempotency. A GET replays on network error and on 5xx, with no side effect. A signed POST only replays on an outright network failure, never on a received response, so as not to risk a double signup.
This client is final but deliberately not readonly at the class level: under Octane, it must stay stateless from one request to the next, and stay mockable in tests. A detail of stateless discipline that avoids leaks between workers.
Why This Discipline on Such a Small Repo
The honest question is: isn't this too much for five pages?
This repo is generated ninety percent and more by AI agents, and human review stays the real bottleneck. A clean module boundary is a guardrail both ways: an agent set loose on Waitlist doesn't spill into Seo by accident, and my reviews stay short to read because a change's scope can be guessed from its file path.
Phase 1 will bring a real product site, and each module today is a foundation, not throwaway scaffolding. And this structure is consistent from one Aubia repo to the next: I find the same reflexes on the desktop cockpit and on the cloud. That regularity, on a project run solo, is well worth a few extra folders.
Getting Under the Site's Skin
What's left is the most visible part: the palette, the glass, and the motion. To get the invitation to the beta 0.1 the moment it opens, the waitlist is there.
This build is chronicled here, article after article. Come aboard: your feedback will shape what comes next.
Join the waitlist