Email

Natanael Alves Gabriel · Brazil, UTC−3 · 4 years shipping production software

I turn ideas into actual realiable solutions.

Backend-heavy full-stack engineer. I design schemas that make the wrong state impossible, then build the apps on top of them.

Open to remote roles. Brazil, UTC−3 — overlapping European afternoons and the whole US Eastern working day.

  1. Olipay B2B + B2C beverage commerce — five apps on one NestJS API. Lead developer, 331 of 406 commits.
  2. TeamMove — orders & products The ordering domain of a field-sales SaaS, schema to screens. 99% of its commits.
  3. forty-monolito Offline-first training platform. Two levels of Postgres row-level security. Source public.
  4. Resumo dos Candidatos Election transparency on official TSE data, with uncertainty modelled in the schema. Source public.
  5. Note 01 — multi-tenancy The same mechanism, and why two products made opposite calls on it.
  6. Note 02 — concurrency Double-booking made impossible by an exclusion constraint rather than by a check.

Selected work

2023 – 2026

Five projects that between them cover the range I actually work in: a commercial payments platform, a sales domain I owned end to end at an employer, a multi-tenant product you can read the source of, a civic-data system, and a game that shipped. Every screenshot on this page runs on seeded demo data — names, plates, phone numbers and figures are synthetic.

Role
Lead developer
Authorship
331 of 406 commits · monorepo
Surfaces
5 apps, 1 API

Olipay Commercial · private source

B2B + B2C commerce platform for beverage distribution

One catalogue serving walk-in consumers and business accounts buying on credit — plus the warehouse, the storefront and the counter behind them.

Five deployable surfaces run on a single NestJS API: a React Native app, a React PWA for iPhone users, an admin backoffice, an in-store point of sale, and the landing page. All five import the same contract package, so a changed response type breaks the build in whichever app is now wrong.

What I built

The Woovi (Pix) payment integration, including webhook signature verification against the published key format and support for multiple receiving accounts. The point of sale, added as a new surface to the monorepo. The whole warehouse model — storage centres, deposit selection at checkout, product packagings, and cost incidents that separate tax from replacement cost. A promotional flyer generator that composes an encarte in-app and exports it to image and PDF. The metrics and trend endpoints behind the dashboard, B2B credit validation, and the loyalty points programme.

How it holds together

Services depend on repository interfaces bound by DI tokens rather than on Prisma, so unit tests mock the interface and integration tests run against a real database. Four route guards, two-factor auth, a global exception filter, structured logging with request tracing, and custom CPF/CNPJ validators. Tested at four levels, up to Maestro end-to-end runs on a device.

The consumer catalogue, and the dashboard behind it. Three further surfaces — the app, the point of sale and the landing page — run off the same API.
  • NestJS 11
  • Prisma 7
  • PostgreSQL
  • React Native · Expo 52
  • React 19 · Vite
  • Turborepo
  • Pino
  • Maestro
Role
Sole author
Period
Mar 2023 – Apr 2026
Authorship
99% of the module’s commits
Surfaces
Schema, Java API, React client

TeamMove — orders & products Employer · private source

The ordering domain of a field-sales platform, built from the tables up

For eight months in 2023 I was the only developer on the Java API — 440 of the 446 commits in that window are mine.

A sales rep standing in a shop needs to place an order against a catalogue that prices differently per customer. I built that domain: orders, order kinds, products, product families and categories, per-customer price tables, and the extra fields a client adds to a product without anyone shipping a release. Seven Java packages, 154 files — 103 of the 104 commits that touched them are mine.

The whole vertical, in one week

It went in from the bottom. The tables and their access layer first, then the REST endpoints — list, detail, create, update, item removal, status transitions — and the React screens opened the next day, on 28 March 2023. The same person designed the schema, wrote the queries against it and built the screens that read them, which is the only reason a domain this wide could land that fast: there was no contract to negotiate, because there was nobody to negotiate it with.

Orders change after they are approved

That is the constraint the whole thing turns on. An order that is edited after approval has to be able to say what changed, who changed it and what the value was before — so in July 2023 I built a generic change ledger underneath it: five tables recording the operation, the table and row it hit, and the previous and new value of every individual field, attributed to a user and a timestamp. It is table-agnostic on purpose — it carries the table name as data, so anything else in the system can be audited by pointing at it rather than by writing another log. Reconstructing an order's history from free-text log lines is a guess; this is a query.

Then I moved it off Java myself

Two years later the platform migrated to a TypeScript back end and my own module was on the list. I ported it — the list endpoint in May 2025, the detail endpoint the week after — and then kept tuning it there, dropping the ORM for a raw query when the list got slow. Most of what I have written has been maintained by someone else. This is the piece I designed, replaced, and then had to live with on the other side.

And the other three and a half years

Orders and products is one module. Around it sits everything else I did on the same platform: the agenda and its month view, the checklist builder and the runtime that fills one in, the ticket workflow with its briefing and approval stages, campaign rankings, goal calculation split across day and month totals, scheduled jobs, the print pipeline that turns a filled checklist into a signable PDF, and a partner-facing external API. Four years across four repositories — the React client, the Java API, the TypeScript back end that replaced it, and the partner-facing external API. I was the primary maintainer of the client and of the ordering domain; the Java API was already six years old when I arrived. The full scope is below →

  • Java · WildFly
  • JDBC
  • PostgreSQL
  • React
  • Redux
  • Express
  • Prisma
  • TypeScript
Role
Sole author
Size
252 files
Source
~67k lines TS
Status
Public

forty-monolito

Training platform for personal trainers

Two apps over one API: the trainer runs their business, and their client logs every set at the gym — where there is no signal.

Trainers get an agenda, a client CRM and income tracking. Their clients get a mobile-first app that works offline first, because that is not a nice-to-have in a gym basement: sets are logged locally and sync when the connection comes back.

Tenant isolation is enforced at two levels of PostgreSQL row-level security — per trainer and per client — so a forgotten WHERE clause cannot leak another trainer's roster. The app stores health-questionnaire responses, which makes that boundary an LGPD obligation rather than a preference. Integration tests run against real Postgres through Testcontainers and assert the isolation directly; Playwright covers the flows end to end.

Left: the trainer’s side. Right: what the client sees at the gym — the same API, a different job.
  • Next.js 16
  • NestJS 11
  • PostgreSQL 17 · RLS
  • Prisma 7
  • Better-Auth
  • Tailwind v4
  • Playwright
  • Testcontainers
Role
Sole author
Source
~3k lines Python
Tests
16, over 5 files
Status
Public

Resumo dos Candidatos

Election transparency on official open data

It shows an incumbent’s voting record only when it can prove the candidate and the office-holder are the same person. Otherwise it says so.

A public platform for the 2026 Brazilian general election. For each candidate it aggregates their published proposals and — for incumbents seeking re-election — their real voting record, attendance and expenses, entirely from official TSE and Câmara dos Deputados open data.

The interesting part is CandidateMandateLink: a materialised, auditable edge asserting “this 2026 candidacy is the same person who holds this mandate”, carrying its match method, a confidence score and its provenance. Below the accepted threshold the page reads incumbência não confirmada rather than attaching a record that might belong to somebody else. Attributing one politician's votes to another is the one bug this project cannot ship, so the uncertainty is modelled in the schema instead of being resolved by a guess.

The candidate register for 2026 only exists after registration closes, so the whole system was built and validated against 2022 and 2024 data — same schemas — and re-points at 2026 through a single config value. Collectors are idempotent and cron-friendly, writing to a raw-ingestion ledger that keeps every derived fact traceable to the file it came from.

The table is the argument. Nothing about the match is implied:

CREATE TABLE candidate_mandate_link (
  id                      uuid PRIMARY KEY,
  sq_candidato            varchar(32)      NOT NULL REFERENCES candidacy,
  mandate_id              uuid             NOT NULL REFERENCES mandate,
  person_id               uuid                      REFERENCES person,
  match_method            matchmethod      NOT NULL, -- cpf_exact | titulo_exact
                                                     -- | probabilistic | manual
  confidence_score        double precision NOT NULL,
  confidence_tier         confidencetier   NOT NULL, -- auto_strong | auto_weak
                                                     -- | review
  is_incumbent_reelection boolean          NOT NULL,
  pipeline_version        varchar(32),
  resolver                varchar(64),
  resolved_at             timestamptz      NOT NULL DEFAULT now(),
  UNIQUE (sq_candidato, mandate_id)
);

confidence_tier is what the page reads, not confidence_score: a review row is displayed as incumbência não confirmada rather than silently attached. match_method and pipeline_version mean a wrong link can be found again later and re-decided, and the unique constraint on (sq_candidato, mandate_id) is what stops two runs of the resolver disagreeing in the same table.

  • Python 3.11
  • FastAPI
  • SQLAlchemy 2
  • Alembic
  • Postgres · pg_trgm
  • polars
  • rapidfuzz
  • Typer
Role
Sole author
Engine
Godot · GDScript
Released
2025
Status
Published, v0.4.0

Detetive Sonora

Released game — itch.io and GameJolt

Seven lanes are the seven degrees of the scale. A chord sounds; you fly to the lane that matches it. You never need perfect pitch — only the interval.

An ear-training game disguised as a detective story, built as my computer science capstone. Nine songs and 121 chords are authored as plain data, three per difficulty tier and drawn at random each run. Each tier changes both the key and the instrument — C on piano, then G on guitar, then D on electric — so memorising absolute pitches gets you nothing and only relative hearing carries over. The reaction window closes from five seconds to three, and below 70% the level repeats.

Three acts, each opening on a fully voice-acted phone call from the defence minister, about a detective chasing a stolen aircraft. In act three you find out the detective is blind — which is the argument the whole design was making anyway.

A run in progress, and the cover. The plane holds the lane that matches the chord currently sounding; the seven lanes are the seven degrees of the scale.
  • Godot 4.4
  • GDScript
  • Windows
  • pt-BR

Want the detail on any of these, or the parts I cannot show publicly? Ask me →

Where I’ve shipped

Employment · source private

Two companies whose products I was paid to build and do not own. No source and no screenshots — so what follows is scope: the domain, the surfaces I worked, and the constraints that shaped the code. One piece of it, the ordering domain I owned end to end, is written up above.

Role
Full-stack engineer
Period
Feb 2022 – Apr 2026
Surfaces
Web client, 3 back ends
Market
Brazil

TeamMove Employer · private source

Field-sales and merchandising SaaS

For two years the Java API being replaced and the TypeScript one replacing it were both in production. I committed to both in the same month, twenty-four months out of twenty-five.

Software for teams whose work happens outside the office — sales reps and merchandisers visiting stores. Visit scheduling and an agenda, dynamic checklists filled in on site, a ticket workflow with briefing and approval stages, sales goals tracked by day and by month, campaign rankings, action plans, and customer clustering that decides who sees what.

Working across the migration

I joined on the Java REST API and stayed through the move to a TypeScript one. Neither was ever switched off in a single cut: features landed on whichever side owned them, which meant holding both schemas in your head and keeping a report query and its replacement answering the same question. That is the unglamorous part of a migration and it is most of the work — the last two years of my history there run down both tracks in parallel.

  • React · Vite
  • TypeScript
  • Express
  • Prisma
  • PostgreSQL
  • NestJS
  • Java · WildFly
  • AWS S3
Role
Full-stack engineer
Period
Apr 2026 – present
Codebase
Java EE, since 2012
Market
Brazilian municipalities

Celk Employer · private source

Public-health systems for city health departments

A required field on a disease investigation form is not a UI preference. It is what the reporting rules say has to be collected, and the form is wrong until it agrees.

Municipal health software — sanitary surveillance, pharmacy and stock control, vaccination — run day to day by city health departments. A Java EE monolith with server-rendered Wicket screens, first committed in 2012 and now well past fifty thousand commits, split across a dozen Maven modules — with newer work going into Quarkus services written alongside it. I work on both sides of that line, which is the same seam as the TeamMove migration above: two stacks in production at once, and a feature landing on whichever one owns it.

What I work on

Disease investigation forms: extended forms, the fields a case cannot be closed without, and period calculation by epidemiological week rather than by calendar month. The licensing side of sanitary surveillance — permit fees, transport licences, veterinary activity records, and the checks that have to pass before a report will generate at all. On the pharmacy side: decimal quantities for prescribed and dispensed medication, merging duplicate medication records, batch selection that carries its own expiry date, stock balance and analysis reports, and the product coding that federal reporting expects.

What the domain costs you

Every change arrives as a ticket against software a city is already running, and goes out through code review before it reaches anyone. Schema changes ship as Flyway migrations held to a naming and DDL standard, because a script that deviates does not fail your build — it fails an upgrade. Most of my fixes here are small and exact for the same reason: a wrong label on a vaccine batch or a missing check on a licence is not a cosmetic bug in this domain.

  • Java EE
  • Quarkus
  • Apache Wicket
  • EJB
  • Hibernate
  • PostgreSQL
  • Flyway
  • JasperReports
  • JBoss

Products built to be sold

Commercial · source private

Three systems I designed and build as products rather than as one-off client work. The source stays closed; the reasoning does not. Every screenshot below runs on seeded demo data.

Model
Shared multi-tenant
Isolation
Postgres RLS
Market
Brazil
Status
Pre-revenue

shop-schedule Private

Booking software for businesses that sell time slots

Two clients can tap the last 15:00 slot in the same second. The database is what stops them — not a check in the application.

An agenda, client register, service catalogue, reminders and cash-basis revenue, for any business whose product is a booked slot. Many establishments share one instance, isolated by row-level security.

Availability is computed on every read, never stored. There is no slot table: free time is derived as opening hours ∩ resource hours − time off − live appointments, then sliced by the service's duration. A stored copy means a cancelled holiday leaves a stale slot on the public page and somebody books an appointment the shop cannot honour. The generator lives in a shared package imported by both the API and the front end, so the public booking page and the counter cannot disagree about what is free.

The two views that must never disagree. Read the column headings — what gets booked is a person, a room or a machine — and note that 09:00, 09:30 and 10:00 sit flush against each other without overlapping.
  • NestJS
  • Prisma
  • PostgreSQL 17
  • btree_gist
  • React · Vite
  • pnpm workspaces
Shape
Reusable template
Consumers
3 apps, 1 contract
Market
Brazil
Status
Pre-revenue

ecommerce-base Private

Brazil-first commerce base, cloned per client

An endpoint cannot return a field its contract does not declare. Not by convention — it is stripped on the way out.

A NestJS API, a Next.js storefront and a React admin in one monorepo, handling Pix, card, boleto, CPF/CNPJ and freight by postcode. It is a template: cloned per client, then configured rather than modified — branding, legal identity, checkout rules, payment provider and carrier are all data.

One Zod schema per contract lives in a shared package. nestjs-zod turns each into a DTO, so a single definition drives request validation, response serialisation and the published OpenAPI document, while both front ends infer their types from the same source. The serialiser strips anything the contract does not declare, which means a password hash caught by a careless Prisma select never reaches the wire. Security by construction rather than by code review.

One clone, dressed as a shop called Ateliê. The branding, the payment methods on offer and the freight rules are all configuration — nothing here is a fork.
  • NestJS 11
  • Next.js 16
  • React 19
  • Zod · nestjs-zod
  • Prisma 7
  • Redis
  • MinIO
Model
Instance per workshop
Source
~19k lines TS
Tests
305, all green
Market
Brazil
Status
Pre-revenue

smart-workshop Private

Management software for vehicle workshops

The customer approved R$ 393,70. Mid-job the mechanic finds the brake pads gone. Nobody can hand the bike back at R$ 592,70 until the customer says yes a second time.

Customers and their vehicles, service orders, a parts ledger, the counter’s cash drawer and the month’s receivables — for the kind of Brazilian oficina that still runs on a paper pad. One schema covers mecânica geral, funilaria, pintura and auto elétrica, which only works because insurance workflows are excluded outright: strip those, and a bodywork job is a long-running mechanical one.

A statute, compiled into a guard

Brazilian consumer law is explicit that a customer cannot be charged for work they did not authorise. That is not a checkbox here. An approval is a row — who approved, through which channel, at what total, carrying a JSON snapshot of every line as it stood — and an order cannot reach delivered while its current total exceeds the last figure approved. The comparison is strictly greater, because charging less than approved was never the problem. The check is one pure function in the shared package, so the browser warns using the exact predicate the API enforces, and APPROVED is unreachable except through the call that writes the record — a status you can set directly is precisely the boolean this design refuses to be.

Isolation by absence

The other multi-tenant systems on this page put a policy in the database and a tenant id on every row. This one has neither, deliberately: each workshop gets its own database and its own instance, so the blast radius is a fact about deployment rather than a query somebody has to get right every time. The application never resolves a tenant and the word does not appear in the schema. The cost is real and was written down before it was accepted — N databases to migrate instead of one. Same mechanism as Note 01, argued the other way.

Two claims made visible. The order was approved at R$ 393,70 over WhatsApp and now totals R$ 592,70, so Entregar is disabled and the banner says why. The brake pad that caused it reads 11 on hand, 1 reserved, 10 available — three numbers recomputed from the four movements beneath them, never incremented.
  • NestJS 11
  • Prisma 7
  • PostgreSQL 17
  • React 19 · Vite 8
  • Zod 4
  • TanStack Query
  • Tailwind v4
  • pdfmake

Technical notes

Three decisions, in detail

The three mechanisms that come up most in my work, and what I actually learned building against them.

Note 01 · Multi-tenancy

Two products, one mechanism, opposite calls

Both forty-monolito and shop-schedule isolate tenants with PostgreSQL row-level security, for the same reason: the threat is not an attacker, it is a query that forgot its WHERE clause. Application-level filtering makes every query a fresh chance to leak. A policy in the database makes the boundary independent of the code that runs above it.

The policy itself is unremarkable — this is forty-monolito's, applied across every tenant-scoped table:

CREATE POLICY tenant_isolation ON "appointment"
  USING      ("trainerId" = app_current_trainer())
  WITH CHECK ("trainerId" = app_current_trainer());

What is worth writing down is that the two projects then made the opposite decision about FORCE ROW LEVEL SECURITY, and both were right.

Where the isolation actually lives

A policy only binds a role that cannot step around it. Three things have to hold or the isolation is decorative:

  1. The runtime role owns nothing. Table owners bypass RLS silently, and a role with BYPASSRLS ignores every policy in the database. Migrations run as an owner; the application connects as a separate role created NOSUPERUSER NOBYPASSRLS that owns no tables.
  2. Tenant context is set with SET LOCAL, inside a transaction. Prisma pools connections, so a bare SET outlives the request and the next one — a different tenant — inherits it. This is the failure that looks like it works in development and leaks under load. There is a regression test that interleaves two requests and asserts the second cannot see the first's rows.
  3. A missing context fails closed. The policy compares against current_setting('app.tenant_id', true)::uuid, which is NULL when unset — and tenant_id = NULL is never true, so an unscoped query returns nothing rather than everything. Never write the OR current_setting(...) IS NULL escape hatch; it converts the safe failure into the catastrophic one.

The disagreement

FORCE ROW LEVEL SECURITY extends policies to the table owner as well. forty-monolito sets it, and grants its migration role BYPASSRLS explicitly — appropriate there, because that system stores health-questionnaire responses and the extra belt is worth the buckle.

shop-schedule deliberately does not, and the reasoning is written into the spec so nobody “fixes” it later. FORCE applies to the owner, which is what runs migrations, seeds and platform administration. The seed has to create an establishment before any tenant context can exist, so under FORCE it simply cannot run. The only way back is to grant the owner BYPASSRLS — which makes FORCE a no-op again. It buys nothing against the threat it appears to address and costs the operator their own tooling.

The protection that is real in both cases is structural: the application role is not the owner. FORCE is a second lock on a door whose first lock is the one doing the work — worth adding when the data is sensitive, not worth breaking your seed script over.

Note 02 · Concurrency

Let the database say no

Two clients open a booking page at the same moment and both take the last 15:00 slot. This is not a hypothetical — it is the single most expensive bug a scheduling product can have, because nobody finds out until both people are standing at the counter.

The instinct is to check for a conflict and then insert. That cannot work at the isolation level an ORM uses by default: both transactions read an empty result, both conclude the slot is free, both write. The window is small and entirely real. Retrying, locking the table, or serialising the endpoint all trade a correctness problem for a throughput one.

The guarantee belongs in the schema:

CREATE EXTENSION IF NOT EXISTS btree_gist;

ALTER TABLE "Appointment" ADD CONSTRAINT appointment_no_overlap
  EXCLUDE USING gist (
    "resourceId" WITH =,
    tstzrange("startsAt", "endsAt", '[)') WITH &&
  )
  WHERE (status NOT IN ('CANCELLED', 'NO_SHOW'));

Four details in there are load-bearing, and I got two of them wrong the first time:

  • The range is half-open'[)'. A 15:00–15:30 and a 15:30–16:00 appointment do not overlap. Closed ranges lose the shop a slot every hour.
  • endsAt is stored, not computed on read. An exclusion constraint needs a real column, and every overlap query wants an index on it.
  • The WHERE clause is why cancelling frees the slot without deleting the row — the history stays, the time comes back.
  • Prisma cannot express this. It goes into the migration as hand-written SQL, and the integration suite asserts the constraint exists — because a schema produced by db push would silently drop it, and the product would appear to work.

The application still checks availability before inserting, so a user who loses the race gets a readable message instead of a constraint violation. But the check is a courtesy. The constraint is what is true.

This is the shape I keep coming back to: put the invariant where it cannot be forgotten, then let the application layer be about ergonomics rather than about correctness. Code you have to remember to write is code that eventually does not get written.

Note 03 · Stock

A counter is only correct if nobody forgets

In smart-workshop, stock is an append-only ledger. On-hand and reserved are not columns that get incremented; they are projections recomputed from the movements on every write. The reason is that a running counter is only correct if every path that touches a line remembers its matching decrement — and when one forgets, nothing fails. The count simply drifts, and you find out when a part has been promised twice.

Two details do the real work. Reservations release by falling out of the set of statuses that reserve, so there is no release step for anybody to forget. And a ledger line can be consumed exactly once because that link is a unique index, not a careful service method. Both are the same move as Notes 01 and 02: the guarantee lives somewhere it cannot be skipped, and the application code above it gets to be about ergonomics.

The stock panel in the smart-workshop screenshots is this note made visible — 11 on hand, 1 reserved, 10 available, all three derived from the four movements listed beneath them. Back to smart-workshop →

What I work with

From the projects above

Nothing is on this list because I read the documentation once. Every item appears in a project on this page, and the line under each group says where.

Languages
  • TypeScript
  • JavaScript
  • Java
  • Python
  • SQL
  • GDScript

TypeScript across most of the systems on this page. Java is TeamMove’s original REST API and Celk’s 2012 monolith — I have shipped production code in both.

Back end
  • Node.js
  • NestJS
  • Express
  • FastAPI
  • Java EE
  • EJB
  • Apache Wicket
  • REST · OpenAPI

Six of the systems on this page are NestJS. Wicket and EJB are what Celk’s Java EE monolith is built from — a codebase past fifty thousand commits that I ship changes into.

Front end & mobile
  • React
  • Next.js
  • React Native · Expo
  • Vite
  • Tailwind
  • TanStack Query
  • three.js

A web storefront, a native app and an in-store point of sale, all typed from the same shared contract package — so they cannot disagree about what the API returns.

Data & persistence
  • PostgreSQL
  • Prisma
  • SQLAlchemy
  • Hibernate
  • Alembic
  • Flyway
  • Redis
  • RLS
  • btree_gist
  • pg_trgm
  • polars
  • rapidfuzz

Postgres under every system on this page. RLS and btree_gist are on the list because Notes 01 and 02 are about what they make impossible.

Testing
  • Playwright
  • Testcontainers
  • Maestro

Integration against a real Postgres in a container rather than a mock of one, because a mock cannot enforce the policy the test exists to prove.

Tooling & the rest
  • Turborepo
  • pnpm workspaces
  • Maven
  • Zod · nestjs-zod
  • Better-Auth
  • Pino
  • Typer
  • AWS S3
  • MinIO
  • JBoss · WildFly
  • JasperReports
  • pdfmake
  • Godot

Monorepo plumbing, object storage, and three separate PDF pipelines — because a checklist, a service order or an encarte nobody can print is a feature that did not ship.

Playable prototypes

Click any frame to play

Vertical slices, each built to answer one design question before any content got made. Single file, no dependencies, no build step — so they run right here, in this page.

Top-down stealth prototype: a guard's vision cone sweeps a dark floorplan while the player hides in shadow, with an alert meter filling at the top.

Signal & Shadow

2D · stealth AI

Guards who genuinely do not know where you are. They see only what falls inside their cone and hear only what is loud enough, then walk off to check a memory that is already wrong. Two files, start to finish.

3D stealth prototype: the player crouches in shadow on a blue-lit greybox level while two guards patrol with visible vision cones and an amber objective glows in a vault.

Signal & Shadow 3D

three.js · zero dependencies

The same question with a camera you can stand behind. Ships as one self-contained HTML file that runs straight off the filesystem, with a development server in about forty lines and a simulation test suite — and no dependencies at all.

First-person recon prototype rendered in amber monochrome: the view through a soldier's eyes down an alley, with a roster of four soldiers and a scrubbable timeline of their recorded past.

Borrowed Eyes

Second-person recon

Every camera is a soldier's head. Wear his eyes, scrub back through what he already saw, and take the courier alive. Built specifically to beat a named failure mode — recon collapsing into hidden-object hunting — before any level got made.

Monochrome prologue: a black square leans left across a pale grid, trailing motion blur, with a legend reading 'hold to lean — that is all you can do'.

Bystander

Twelve seconds

A prologue in which you can only lean, and watch. The legend never lists a key you are not yet allowed to press, because at the start you can do exactly one thing. Three files.