Visitors browse, filter, compare, calculate, read articles and guides, and request a product through a consent-based lead form. Editors maintain issuers, credit cards, reviews, offers and content through the CMS and triage leads.
Draft
Deferred capabilities
Pension funds and home loans have schema and API foundations but no public vertical pages or CMS maintenance path. Accounts, saved comparisons, alerts, Clerk, Redis and document storage are deferred.
No automated ingestion or bank scraping exists. Editors are the only source of canonical product updates. Product request CTAs open an in-page form rather than redirecting to an issuer.
principles
Principles
Principle
Consequence
SEO first
Public content arrives as server-rendered HTML with metadata and structured data.
Low operational overhead
Two deployable application services and one shared PostgreSQL database on Railway.
Extensible domains
Shared product fields plus vertical-specific tables and typed interfaces.
Transparent freshness
Show the last edit time and derive staleness from age; retain visibly stale products.
Privacy and consent
Public exploration is anonymous; lead collection requires explicit consent.
Proportionate security
Editor Basic auth, a private API, validated boundaries and parameterized SQL.
Measurable fitness
Performance targets include LCP <2.5 s, CLS <0.1 and INP <200 ms; targets are not asserted production measurements.
tech stack
Technology stack
Layer
Implementation
Rationale
Web and BFF
Next.js 14.2.35, React 18, TypeScript
Server-rendered public pages and same-origin route handlers (ADR-002).
Committed output, no browser JavaScript; hosting setup remains manual.
context
System context
C4 context — people and external systems
containers
Containers
C4 containers — deployable services and data
Admin UI and proxy handlers are modules of the web service, not separate deployments. Dashed connections indicate Draft capabilities. No ingestion service exists.
api components
API components
FastAPI modules
The app factory wires stub repositories for contract tests and SQLAlchemy repositories when a database URL is supplied. Admin writes record audit entries; freshness is computed on read. The retention command is a separately scheduled process, with its schedule configured manually.
web components
Web components
Next.js layers
cms and leads
CMS and leads modules
CMS and lead ownership
rendering and seo
Rendering and SEO
API-backed pages use force-dynamic: Railway build containers cannot reach the private API. Data is fetched on each request rather than baked into an error page. Static educational and policy pages can render at build time; interactive calculators run client-side within their rendered page shell.
Current URL
Surface
Rendering / index policy
/
Homepage
Static shell
/tarjetas-de-credito/
Credit-card hub
Dynamic SSR
/tarjetas-de-credito/catalogo/
Catalog with filters
Dynamic SSR; filtered variants noindex
/tarjetas-de-credito/[issuer]/[product]/
Product detail and review
Dynamic SSR; JSON-LD financial product and breadcrumbs
/comparar/tarjetas/?ids=…
Anonymous comparison
Dynamic SSR; noindex, follow; canonical hub
/tarjetas-de-credito/buscador/
Session-only card finder
Dynamic SSR; answered query variants noindex
/tarjetas-de-credito/articulos/ and /articulos/[slug]/
/privacidad-solicitudes/ and /privacidad-solicitudes/eliminacion/
Lead privacy and deletion request
Static page shell
/admin/**
Editor CMS and inbox
Dynamic; noindex; Basic auth
Metadata and structured data
Public pages use Next.js Metadata or generateMetadata for title, description, canonical and Open Graph data, with es-CR language alternatives where configured. The root document uses lang="es-CR". JSON-LD is rendered in server HTML; helpers serialize it safely. Detail pages expose financial-product and breadcrumb data; hubs, calculators, articles and guides emit their relevant schemas.
Canonical and linking strategy
The implemented canonical paths generally include trailing slashes. Catalog query variants remain anchored to the catalog path; comparison pages point to the credit-card hub and use noindex, follow. Comparison IDs are normalized by the URL codec. Hubs link to catalog, calculators, finder, articles and guides; contextual links return users to products and comparisons. No user-specific ranking or saved-comparison dashboard is shipped.
Sitemap and robots — Draft
Draft The former architecture described next-sitemap, per-vertical sitemaps and robots.txt rules. None is installed or committed. Per-page robots metadata exists, including the admin layout and comparison noindex policy; automated sitemaps remain deferred.
Obsolete URL claims removed: standalone issuer and named filter landing pages, pension/home-loan public pages, /mi-cuenta/**, /acerca-de/, /privacidad/ and the generic /comparar/ guide. Their absence is recorded rather than presented as current behavior.
security and privacy
Security and privacy
Control
Implemented behavior
Admin access
Next.js gates /admin/**; FastAPI independently gates /api/v1/admin/**. Both use Basic credentials from ADMIN_USERNAME and ADMIN_PASSWORD with constant-time comparisons. Missing configuration returns 503; missing or invalid credentials return 401. Lead route handlers forward caller authorization. Several CMS proxies inject configured credentials; see the verified proxy authorization gap.
Transport and boundaries
Railway terminates public TLS. Web-to-API requests use the private network; the API is not intended to have a public domain. No Clerk integration or end-user RBAC is installed.
Next.js sets X-Frame-Options: DENY, X-Content-Type-Options: nosniff, strict-origin-when-cross-origin referrer policy and a Permissions-Policy disabling camera, microphone and geolocation.
Rate limiting
FastAPI counts requests per client IP in a process-local sliding window, 60 per minute. Shared Redis counters are deferred.
Consent and minimization
The lead form requires consent and a policy version. The repository inserts the lead and consent record atomically, storing a SHA-256 IP hash rather than raw IP. Catalog, calculator and comparison inputs are session-only.
Session attribution
urza_session_id is random, host-only, SameSite=Lax, expires after 30 days and is Secure in production. It is deliberately not HttpOnly because the client beacon reads it; it grants no authentication authority.
Lead retention and erasure
Admin actions anonymize a lead or all leads matching a deletion-request email. Discarding a lead anonymizes it immediately. The scheduled command anonymizes leads older than LEAD_RETENTION_DAYS (default 365); the cron schedule is manually configured.
Logging and notifications
Lead notification logs identify the lead by ID and avoid name/email. SendGrid receives name/email in the internal notification body; failures do not block lead submission.
Verified privacy boundary and limitations
The intended PII boundary is lead capture and its consent records. The existing lead_deletion_requests table also stores the requester’s email and processing notes. Processing a request marks it processed without clearing that email; the retention command only anonymizes leads. This is an existing discrepancy with the stated narrow PII boundary, not a new storage surface introduced by this documentation.
Lead anonymization clears name, email and notes while retaining non-identifying product context. Consent records and session attribution remain. The deletion-request record and SendGrid/team mailbox copies are not removed by that operation. The documentation describes these limits rather than asserting complete erasure across systems.
Draft controls
Draft End-user authentication, per-editor identities and revocation, MFA, RBAC, Redis rate limits, a full Content-Security-Policy/HSTS policy, formal incident procedures and legal/processor agreements are not established by the code inspected here. The old target-state statements about automatic export, 48-hour account erasure and specific regulatory deadlines are not presented as shipped guarantees. CORS middleware is unnecessary for the same-origin browser path and is not configured.