Command: cat projects/tourism-portal.md
A high-end tourism portal prototype, with a data contract ready for the back end
A clickable, front-end-only prototype with fictional data of a high-end tourism portal for a lake region in inland Brazil, shown to a client. It brings together lodging, food, boat trips, real estate, events and services from local partners, and every conversion ends in a pre-filled WhatsApp conversation, with no booking and no payment. The shared types have the shape of a real API, so the back end can come in without rewriting the screens.
open the demoa simulation of the system with fictional data · 6 screens
- Role
- Sole engineer, from the front end to the delivery package
- Period
- Sep 2026 – Sep 2026
- What I did
Me: Product, architecture, data contract, every screen of the portal and the panel, quality gates, tests and the delivery documentation for the client and for the back-end team.
Private project, described without identifying the client, product or company.
Diagram
In a monorepo with pnpm workspaces, a contracts package with types only and no runtime defines the shape of the data. An async repository layer hands that data to the screens: today it reads fictional data from memory, and the API plugs in there later. The public portal and the internal panel ship as separate bundles, and the panel publishes to the portal through browser storage. The portal builds every conversation with the single WhatsApp link builder, and a delivery gate runs types, lint, tests, build and a scan of the bundle.
Context
A client wanted a high-end tourism portal for a lake region in inland Brazil: lodging, restaurants, boat trips, experiences, real estate, event venues and services from local partners, each item with its own page. This phase was front end only, with fictional data, so the client could see the product working before any server existed and another team could build the back end later. It is a prototype shown to the client: it was not published, it has no back end and no real partner, and there is no business result. The portal does not book or charge; as a legal decision, every conversion ends in a pre-filled WhatsApp conversation with the house.
What I built
- The portal. A home page with the region drawn in SVG, a general search with filters and per-facet counts, an index per category, a stay search that checks each house’s calendar and minimum nights, and listing pages in seven shapes: lodging with a calendar, restaurants with a menu and an order, boat trips with a route and time slots, experiences, real estate, events and services. Every price reads “from”, with the date it was updated and what is and is not included.
- The conversion. A single builder writes the WhatsApp message from a template for each intent, with the listing code and a short origin reference, derived deterministically from an alphabet without letters and digits that sound alike on the phone. Without that reference, the portal would have no way to show the partner where the customer came from.
- The partner page. A landing page with the house’s identity, outside the portal’s frame, with its color tested for contrast.
- The internal demo panel. What needs attention today, conversations by status with the median time to first reply, partners, listings and site content, which publishes to the portal in real time, in the same browser. The entry screen is a declared mock-up: it authenticates nobody, on purpose, and the panel warns that content is saved only in that browser.
- The delivery documentation. Written for two readers, the client and the back-end team, with each document stating its reader: architecture, API contract, database schema, authentication and roles, quality gates and the closing measurement.
What I would do differently
I would grow the end-to-end coverage at the start, not at the very end. The two defects it found had been in the product for a while and had passed visual review; one of them only showed at 390 px wide.
Constraints
- Front end only in this phase, with fictional data flagged in the data itself.
- The portal promotes and connects; it does not book, charge or hold visitor accounts, as a legal decision.
- No network request at run time, not even for fonts or images.
- Another team will build the back end, so no screen may depend on where the data comes from.
- Copy rules (full Portuguese accents, no em dashes) checked by a script.
Decisions
A data contract in a types-only package
- Context
- The back end would come later, built by another team, and types copied from one side to the other would drift at the first new field.
- Choice
- A contracts package with types only, no runtime and no dependencies, consumed as source by the app: branded ids and URLs, money in integer cents, ISO dates and closed unions per category.
- Gains
- A shape mismatch becomes a compile error in the front end.
- The back end starts additive, implementing what the contract already describes.
- Costs
- More ceremony to change a field in the middle of prototyping.
An API shape from day one
- Context
- In this phase the data lives in memory, but wiring up the API could not turn into a rewrite of the screens.
- Choice
- An asynchronous repository layer, with typed errors instead of exceptions, cancellation on every method and opaque cursor pagination, even while reading an array.
- Gains
- Connecting the API swaps the functions' implementation, not the components.
- Costs
- Asynchronous code and error handling for data that is still local.
A deterministic calendar
- Context
- The calendar needed stable fictional occupancy: a Saturday that was free in rehearsal had to stay free in the presentation, and the tests could not flake.
- Choice
- Occupancy computed from a hash of the listing and the date, with the reference day passed as a parameter and never read from the clock.
- Gains
- Same input, same calendar.
- The generator stays useful as a test source once real data exists.
- Costs
- It is made-up data that looks real, so every screen has to say that no date is held.
The partner page outside the portal's frame
- Context
- Each partner's page had to feel like the house's own site, with its color and rhythm, without losing accessibility.
- Choice
- A landing page with its own header and footer; the house color is contrast-tested in both directions and, if it fails, the page falls back to the portal's ink. Hiding the frame with CSS was rejected, because it would leave invisible navigation in the tab order.
- Gains
- The house gets its own identity without giving up minimum contrast.
- Costs
- The portal loses presence on exactly the page that converts most.
Delivery gates run by a machine
- Context
- A public prototype cannot ship real data, secrets or the internal panel in its bundle, and visual review does not catch everything.
- Choice
- A gate in sequence: copy, accents, types, lint with security and accessibility rules, tests, build and a scan of the finished bundle that fails on a real phone number, e-mail, Brazilian tax IDs, keys, disk paths, source maps and any panel code.
- Gains
- What used to depend on discipline becomes a build failure.
- Costs
- Every round of changes waits for the whole gate.
Stack and why
- React 19 and React Router 7
- Portal and panel as two entry points and two bundles, with no code shared between them.
- TypeScript 5.9
- The types contract consumed as source; a stable version chosen with whoever inherits the project in mind.
- Vite and pnpm workspaces
- A monorepo with the app and the contracts package.
- Tailwind CSS
- The palette tokens, in a single light theme with checked contrast.
- Vitest
- Contract, calendar, message builder and the panel's publishing.
- Playwright with axe
- End-to-end with accessibility at 1440 and 390 px; the delivery screenshots come from the tests themselves.
- ESLint 9
- Security and accessibility rules, and fetch banned at run time.
Results
A test fails the build if any button says "book", "buy" or "pay", and a gate over the finished bundle fails on personal data, keys, source maps and panel code.
audited private repositoryThe repository's tests and scripts(Audit of the private repository, Sep 2026)1,237 unit test cases passing, in 22 files, in the delivery closing measurement.
self-reportedClosing measurement of the delivery package (2026-09-22)306 end-to-end cases with an accessibility audit, whose only known failures, two of them, are in the internal panel, outside the delivery scope.
self-reportedClosing measurement of the delivery package (2026-09-22)Growing end-to-end coverage from 240 to 306 cases found two defects that had passed visual review: a photo mosaic overflowing by 24 px at 390 px wide, and a description list with one nesting level too many on every listing page.
self-reportedClosing measurement of the delivery package21 technical decisions recorded, each with the rejected alternative and the cost of reopening it.
self-reportedDecision log of the delivery package
Security angle
Attack surface
- A public bundle anyone can download, with the whole catalog.
- WhatsApp links built from the listing, the dates and the order.
- Route and search parameters coming from the URL.
- An internal panel that publishes content to the portal through browser storage.
Controls in place
- A single WhatsApp link builder: validated number, encoded message and nothing taken from the URL or typed text.
- Route parameters validated and never used as object keys; seven literal routes instead of one parameterized route.
- The security policy in one file, which generates the hosting headers at build time and is audited against the served build.
- No network request at run time: the linter bans fetch.
- The document the panel publishes is treated as hostile input and rebuilt field by field against closed lists.
- The panel has no link from the site, is noindex, ships in a separate bundle, and a gate proves none of it leaks into the public one.
What I would test today
- A tampered document in browser storage trying to inject content or links into the portal.
- Route parameters such as __proto__ and constructor.
- Line breaks or extra text injected through the URL into the WhatsApp message.
Evidence
- audited private repositoryAudited private repository(Audit of the private repository, Sep 2026)
- self-reportedDelivery package with the closing measurement