Lander Deploy (S3)
Actor flows · 07 Sep 2026

Who does what

Seven flows, from the system map down to the finest mechanism. Each one answers a single question, and answers it by drawing — not by naming.

FromDaniel Souza, CTO
OperatorsRolen · Santie
Sites live51
StatusBuilt and serving
Verified againstsource and API, 07 Sep 2026

Level 0 · the whole system

Two chains that meet only at the bucket

The system has two halves that barely speak to each other: one publishes and the other serves. They meet in exactly one place — and that is what makes the design safe.

THE PUBLISHING CHAIN OperatorRolen · Santie Panellander-ref Developer lander-contentgit · 51 sites GitHub Actiondeploy-s3.yml S3router-pagesthe meeting point SOLE WRITER · OIDC THE SERVING CHAIN RouterApache + PHP CloudflareDNS · proxy · TLS Visitor NoIPFraudcloaker reads only the cloaker decision, on every request
personour systemexternalthe single meeting point
The two chains touch at a single point: the bucket. Nothing writes to it but Actions, and nothing reads from it but the router. All of the design's security comes from that — there is no path by which a person reaches served content without going through a commit. Verified in source: src/app.ts imports GitPublisher and does not import runDeploy. The panel publishes through git; it has no route to S3.

Level 1 · human actor

The operator publishes a page

The actor the system exists for. They are not developers, and the path has to fit on one screen without a single word of infrastructure.

PUBLISHING AND BEING SERVED ARE TWO EVENTS — ON PURPOSE Operator Panel Repository Action S3 create site · upload pages · build campaign validate the manifest — schema · host · files · campaigns git push, under a lock push to main triggers the pipeline inject base href · upload the diff · remove orphans duplicate host → REJECTED host that is not a hostname → REJECTED
Publishing and being served are two events, on purpose. They publish on their own, in seconds, as often as they like — creating a bucket prefix costs cents and serves no one. What makes a domain reach people is the gate, which lives outside the panel. That is why giving them autonomy opens no hole.

Level 1 · the panel

What the operator sees

No longer a mockup. The panel runs on the reference server, reachable only from the office address range, and since 07 Sep it reads real traffic from the router's own log rather than counting what is in git.

On the screenWhy it is there
State of both trees before anything is touchedthe operator sees what is live before deciding to change it
Money weights must total exactly 100prevents the blank money page — enforced by the validator, not by a warning
The clid, readable and stableit will live in reports and tickets for years
Exactly what gets written into the campaignno gap between what the panel shows and what the cloaker receives
Requests, bytes, errors and silent sites, last 24 hread from the router's access log, on the same machine
Three distinct silences, never one zerocould not read · the log does not name this site · measured, and it is zero
A state, never a greyed-out buttonthe panel does not control DNS or TLS. Showing reality is honest; offering a button that does nothing is not
The largest product gap sits here. The panel creates campaigns. It cannot edit or delete one — that means hand-editing manifest.json, which asks the operator to become a developer. The panel says so on its own screen rather than hiding it.

Level 1 · human actor

The developer commits — and why the two doors converge

The most important architecture decision in this project was not made for elegance. It was made because the alternative loses content in silence.

This is not a preference: it is what prevents silent loss. With two writers, the next developer deploy erases the page the operator uploaded — and the repository cannot arbitrate, because it never saw that page. By making the panel emit a commit, the conflict now fails in the face of whoever is writing instead of vanishing overnight.

And there is now a second pipeline, with a real gate

PipelineRepositoryGateHow it reaches the machine
deploy-s3.ymllander-contentmanifest validation, in the panelOIDC → S3. Sole writer.
deploy-refserver.ymllander-deploy-platformtypecheck && test && build — the deploy job will not start if any failOIDC → IAM role → SSM SendCommand. No SSH key in GitHub, no port 22 open.

Level 1 · external actor

The visitor clicks

The path where the revenue lives. Every arrow here has an invariant tied to it, and every invariant exists because the alternative breaks without warning.

THE MONEY PATH — ORIGIN FETCH, NEVER A REDIRECT Visitor Cloudflareproxied · edge TLS Routerindex.php NoIPFraudcomputes only money.phpALLOWED falls throughBLOCKED → safe page S3 origin fetchbytes into the same response A redirect and an origin fetch are not the same thing. A 302 changes the URL in the address bar. Only fetching the bytes and writing them into the same response is acceptable on the money path — and no uptime monitor will ever flag the difference.
Fail-to-safe is structural, not configured. If anything on the cloaker's path fails — exception, API down, timeout — execution continues and the safe page renders from the same file. It stops being a config field that can be wrong in 1 of 60 campaigns and becomes the file's default behaviour.

Level 2 · the files

What lives on the server and what the operator uploads

Four files explain the entire system: two of ours, on the server, identical for every domain — and two of theirs, in S3, which are pure HTML.

SITE (domain) └── CAMPAIGN 1:1 with the clid ├── safe page 1 ← the campaign's own page └── money pages N ← weights totalling exactly 100

A site has N campaigns — one per traffic slice (geo, source, ad account).

A correction worth the whole design: the snippet does not go inside the safe page. On the legacy boxes the vendor generates a PHP fragment to paste at the top of the page, which is why the page had to be .php — meaning the file someone uploads is executable. In this design the snippet lives in index.php, which is ours and version-controlled. The pages they upload are static HTML. Nothing an operator sends can execute. The guardrail has already proved itself in production: it rejected 537 files carrying the executable bit during the migration — SVG flags and the Push Notification Tool had arrived with +x set.

On the server — two files, ours, version-controlled

index.phpentry point · also the safe path
<?php
ob_start();          // the snippet emits output
                     // before our headers
require __DIR__.'/lib.php';

$m = manifest(host());
$c = $m['campaigns'][slug()]
     ?? $m['campaigns'][$m['default_campaign']];

// the PATH identifies the campaign
$_GET['clid'] = $c['clid'];
include APPLOC.'go.php';   // only computes
                           // $isItSafe and returns
if ($isItSafe) { noIpFraud(); }
// ALLOWED -> money.php and ENDS
// BLOCKED -> falls through ↓

header('Content-Type: text/html');
s3_stream($c['safe']['key']);

It is also the safe path. BLOCKED does not halt — execution continues and the safe page is served by this very file.

money.phpallowed · fails loud
<?php
// go.php calls: money.php
// ?c=echowave-mic&page=chest
$m = manifest(host());
$c = $m['campaigns'][$_GET['c'] ?? ''] ?? null;
$e = $c['money'][$_GET['page'] ?? ''] ?? null;

// dictionary lookup, never
// path concatenation
if (!$e) {
  alert('money_page_missing', host());
  http_response_code(404); exit;
}

header('Content-Type: text/html');
s3_stream($e['key']);   // same response

Fails loud. A missing money page is a bug — it must not turn into silence.

The failure asymmetry is deliberate. Money fails loud (404 plus an alarm), because the cost of hiding is losing revenue in silence. Safe fails soft (falls through to the default), because the cost of shouting is looking broken to whoever is reviewing the ad.

In S3 — what the operator uploads

<host>/safe/…/index.htmlpure html
<!doctype html>
<html lang="en">
<head>
<base href="/_s/echowave-mic/">
<!-- injected by the pipeline at deploy -->
  <meta charset="utf-8">
  <title>EchoWave USB Premium Microphone</title>
  <link rel="stylesheet" href="css/style.css">
</head>
<body>
  … store, review, static content …
<!-- ZERO PHP · ZERO snippet -->
</body></html>

This is what an ad reviewer sees. It has to be a real site.

<host>/money/…/index.htmlpure html
<!doctype html>
<html lang="pt-BR">
<head>
<base href="/_m/chest/">
<!-- disjoint prefix, so assets never collide -->
  <meta charset="utf-8">
  <title>Bet7K — Baú da Sorte</title>
  <link rel="stylesheet" href="css/style.css">
</head>
<body>
  … offer, CTA, pixel …
<!-- ZERO PHP · ZERO snippet -->
</body></html>

Served at the same URL as the safe page, with 200, no redirect.

The <base href> is what makes assets work without editing the pages. Both trees can carry css/style.css under the same name; the prefix injected at deploy sends each one to look in its own space, and the asset handler does not need to know which branch the visitor received. It also has a cost, and it was paid: the injected base turned every link into /host/host/…, which is why acceptance check 8 now requires a 301 on duplicated URLs.

Level 2 · mechanism

The pipeline decides: create or update

A machine actor. It runs identically for both doors — that is what guarantees the operator and the developer get the same validation and the same gate.

Creating is free; serving is the gate. In the old design, a find-and-replace in the repository would provision hundreds of servers. In this one it would provision hundreds of empty folders nobody can reach — the cost of a mistake dropped from an incident to cents.

The manifest contract

FieldWritten byRule
schemathe panelconstant 1. No default — reserved for a future breaking change
hostthe panel, authoreddeclaration of where the site goes. Beats the folder name when the two differ
typethe panelcloaked or direct. Absent or unrecognised serves nothing — never inferred from an empty campaign set
files[]the Actionderived from the repository tree. Never hand-written
campaigns{}the panel only{} is valid — a freshly created site has none yet
default_campaignthe panelwhere a bare-root request falls. Required once a cloaked site has a campaign; must be absent on a direct site
Three guards exist because host is authored rather than derived. Two folders declaring the same host fail the deploy — uniqueness used to be guaranteed by the file system and a JSON field does not guarantee it. A host that does not look like a hostname is refused. And every divergence between folder name and declared host is printed, so it never happens quietly. Renaming the host of a live site leaves the old prefix in S3, still served to anyone pointing that domain at us. There is no automatic withdrawal for that case — it is a human decision, written here instead of discovered later.

Level 2 · mechanism

How a campaign is born

The finest mechanism in the system, and the only one where a mistake raises no error. Read in the vendor's code, not in their documentation.

The clid can be ours — and that is what makes the query cheap. The product's loop only draws at random when the name arrives empty or already exists; a brand-new name passes straight through. With an identifier derived from the site and the slice, "does it already exist?" becomes a keyed query against the local database — no network, no catalogue, no third party on the critical path.
TWO OWNERS OF ONE RECORD, NO WRITTEN RULE Operator manifest.jsonversioned, reviewable reconcilerruns on deploy NoIPFraudthe live campaign vendor panelmanual edit, unversioned the next deploy OVERWRITES this edit
Discovered in production, not in review: a campaign showed active=0 at the origin and active=1 in the cloaker. It was not a bug — it was a manual edit made to run a test. The mechanism that surfaced it is conferir-campanha.py, which compares field by field.
Open decision, with no owner and no date. Three resolutions are possible — the manifest wins and panel edits are temporary; the panel wins after the first migration and the reconciler only ever creates; or field by field. Until one is chosen, run conferir-campanha.py before any redeploy of a cloaked site: it prints exactly what would be overwritten.

Level 2 · what goes wrong

The failure modes that make no noise

All of these leave the site answering 200, with monitoring green. This is the list of what the panel and the gates exist to make audible — and, unlike in August, every line now has a mechanism against it.

#Silent failureWhat the visitor getsWhat now catches it
1Tracking subdomain stops answeringa perfect page with a dead CTAgate 5 — track. and journey. must resolve and answer
2Money page missing from the campaigna blank or default page on paid trafficmoney.php returns 404 and alarms; weights must total 100
3Cache leaks which side was servedthe decision is readable from the header alonegate 7 — cloaked sites must answer no-store
4The cloaker sees the CDN's address, not the visitor'severy decision made on the wrong inputgate 9 — measured through the /_ipprobe probe, never inferred
5Content deployed but the domain still points at the old boxthe old site, indefinitelystill open the Action does not check this. It caused a real question from an operator on 01 Sep
6An object exists in the bucket that was never meant to be reachablean internal file served publiclygates 3b and 3c — the manifest and every other object must 404
7A manual edit in the vendor panel is silently overwrittenthe campaign reverts mid-flightstill open conferir-campanha.py shows it, but only if someone runs it
This table is the reason the project exists. Seven ways to lose revenue without anything turning red. None of them is detectable by availability monitoring — which is why the alarm belongs on the money/safe ratio per domain, never on uptime.

The nine acceptance gates

Run by testar-site.py before and after every origin switch. A site is approved only when all nine pass.

#CheckWhat it prevents
1The domain answers, and we record who served itwithout this, nothing below means anything
2Every manifest file byte-identical to the repositorydiscounting only the <base href> the pipeline injects
3Prefixed paths: assets 200 with the right type, documents 404serving HTML where an image was requested
3bmanifest.json returns 404the manifest must never be public
3cEvery other object in the bucket returns 404serving a file that exists but was never meant to be reachable
4Valid TLS for the domain, and who issued it
5track. and journey. resolve and answera 200 page with a dead CTA — traffic paid for, revenue zero
6Outbound /click links resolve — without issuing a GETrequesting them would register a false click in RedTrack
7Cache headers do not leak the cloaker decisionthe served side was readable from the header alone
8Duplicated URLs redirect with a 301the injected base turned every link into /host/host/…
9The cloaker sees the visitor's addressmeasured through /_ipprobe, never inferred from the page
Rules the verifier itself follows It resolves through 1.1.1.1, never the operating system cache — otherwise it tests the old origin after a switch and reports success. Unknown and not-applicable never count as a pass. Origin is read from an explicit header, not inferred from an artefact on the page: the previous version inferred it from the <base href> and broke on the day we started removing it.