Planning & architecture deliverable - not a live store
The Vocho shop,
built to cost less.
A complete, actionable architecture for a classic VW autoparts business in Mexico - Vocho, Combi, Brasilia, Caribe, Safari - hosted on Cloudflare Workers, Pages, D1 and R2, gated by Cloudflare Access, with Stripe and PayPal checkout. Every cost figure is verified against the vendor's own published pricing, and each claim is tagged.
01
Executive recommendation
What to build and why, in one screen. This is a recommendation, not a verified fact about your business - the numbers come from the vendor pages.
Run the whole store on Cloudflare's free tier first: a Pages site (or a single Worker with static assets), one D1 SQLite database, and one R2 bucket for product photos [1][2][3]. Protect the private product-editor area with Cloudflare Access, so the owner signs in with an email link and nothing else is exposed to the public internet. Take card payments through Stripe [6] and add PayPal [7] as the fallback many Mexican buyers expect.
- Recommendation For a nontechnical, mobile-first owner with a modest catalog, start with a minimal custom admin on D1 + R2 rather than a full headless CMS. It is the cheapest to run, the simplest to keep working, and it needs no server at all.
- Recommendation Skip WordPress entirely. It brings a database you can't version, plugins that rot, and an admin that is slower and heavier than the whole store needs.
- Recommendation If a visual content editor becomes a real need as the catalog grows, adopt Payload 3.0 (or Directus) then - as a migration, not on day one. Payload runs on Cloudflare Workers and keeps your data portable [12].
- Recommendation Ship payments defensively from the start: signed webhooks, idempotent order creation, and stock reservations that release on timeout. Cheap to build once, painful to retrofit.
02
Architecture
Everything runs at the edge on Cloudflare. There is no server to patch, no VPS to rent, and no database to babysit.
The store is a static front end plus a thin serverless API. Visitors get HTML and assets from the CDN; only checkout, search, and admin writes touch a Worker. The diagram below is the whole system.
┌────────────────────────────────────────────┐
│ Cloudflare network │
Visitor browser ──────▶ │ │
│ Pages / Worker (static assets + API) │
│ │ │ │ │
│ ▼ ▼ ▼ │
│ ┌──────┐ ┌──────┐ ┌───────┐ │
│ │ D1 │ │ R2 │ │ KV │ │
│ │ SQL │ │images│ │tokens │ │
│ │orders│ │ │ │sessions│ │
│ └──────┘ └──────┘ └───────┘ │
│ │ │
│ ▼ (server-side webhooks, never │
│ ┌──────────────────┐ browser-only) │
│ │ Stripe API + │ │
│ │ PayPal Partner │ │
│ └──────────────────┘ │
└────────────────────────────────────────────┘
│
▼
Email (order + stock alerts)
Admin owner ──▶ /admin gated by Cloudflare Access (Zero Trust)
(email-link login, no public password to leak)Pages vs Worker
Either works. Pages is a managed build pipeline with Git deploy and a free 500 build/month allowance [4]; a bare Worker with Workers Static Assets is the same runtime with one fewer moving part. Both surface the same Worker API for the dynamic bits.
Data: D1
D1 is Cloudflare's serverless SQLite. It bills on rows read, rows written, and storage - not on hours - and it scales to zero, so an idle shop costs nothing [2]. A parts catalog and order history are a textbook fit: small, relational, and rarely written.
Images: R2
R2 holds product photos and serves them with zero egress fees - unusual for object storage and ideal for a photo-heavy catalog [3]. Free tier covers 10 GB-month, far beyond tens of thousands of compressed part photos.
Admin security: Cloudflare Access
Cloudflare Access is an identity-aware reverse proxy: the /admin path only opens to approved emails via one-time login links. The owner gets a login screen far simpler than a password manager, and you never build auth yourself. The Zero Trust Free plan covers up to 50 users [16].
Sessions and rate limiting: KV
Use Workers KV for short-lived tokens and lightweight rate limiting on checkout. Keep real money state in D1 (KV is eventually consistent - fine for tokens, wrong for stock).
03
Admin & CMS options
Six open-source or zero-cost ways to run the product editor, judged for Cloudflare fit, real operating cost, and mobile usability for a nontechnical owner.
| Option | Model | Cloudflare fit | Real cost caveat | Mobile for a nontechnical owner | Read |
|---|---|---|---|---|---|
| Strapi | Self-hosted headless CMS | Runs on a Node/PG server; needs a VPS or managed Postgres - not native to Workers | Assumption | Needs a server + database you operate (real money and upkeep) | Heavy to self-host |
| Directus | Self-hosted headless + data studio over SQL | Self-host on VPS; data stays in your own SQL | Admin can wrap D1, but the usual path is a Node server + Postgres/MySQL | Assumption | Capable but complex |
| Payload 3.0 | TypeScript CMS that runs in a Worker | First-class Workers support; deploys with your store | Free/open core; Cloudflare paid plan floor may apply for prod | Recommendation | Good middle ground |
| Decap CMS | Git-based, static CMS | Excellent (file-based, no server) | Needs Netlify Identity or OAuth; content is files, not relational | Clunky for parts inventory; fine for a blog, weak for SKU data | Assumption |
| TinaCMS | Git-based visual editor | Good (files + API) | Open core; cloud hosting is paid, self-host is DIY | Visual but inventory + reservations need custom fields either way | Assumption |
| Custom D1/R2 admin | Small custom app you own | Native - just another Worker screen | Near zero at launch; it is your code to maintain | Recommendation | Best for low/mid volume |
The honest split is between database apps and Git/file apps. Parts inventory, reservations, and orders are relational, fast-changing data - that points at Strapi, Directus, or Payload over the Git-based editors (Decap, Tina), which are strongest for editorial content, not stock levels.
Against the goal of zero operating cost, every self-hosted Node/Postgres CMS needs a server you must patch and pay for - that is real money and real risk for a hobby-adjacent business. Payload 3.0 removes the server by running in a Worker. A custom minimal admin removes more: no CMS learning curve, no framework updates beyond your own code, and a UI you can shape into a few big, tappable screens the owner can drive on a phone.
- Verified Strapi is self-hosted; you bring the Node runtime and a database [10].
- Verified Payload 3.0 markets first-class Next.js and Cloudflare Workers deployment [12].
- Verified Decap CMS is an open-source Git-based content manager [13]; TinaCMS is also Git-based [14].
04
Recommended path
A default that avoids WordPress bloat without reinventing e-commerce from scratch.
Phase 0 - the default build
- Cloudflare Pages (or a single Worker) serving the storefront.
- One D1 database with the schemas in section 05, created idempotently on first run.
- One R2 bucket for images, served through the CDN.
- A small custom admin - protected by Cloudflare Access - with three screens: catalog (add a part), stock, and recent orders. Big touch targets, plain language, works in Spanish.
- Stripe Checkout + PayPal with signed webhooks, idempotent orders, and reservation-based stock (section 06).
This is not reinventing the wheel: payment flows, tax, delivery and the hard e-commerce plumbing are handled by Stripe and PayPal. You only build the small surface your business actually needs - catalog, compatibility, stock, orders - which is exactly the part off-the-shelf e-commerce platforms force into their mold anyway.
Phase 1 - when the catalog outgrows the custom admin
If editing parts in your custom screens starts to feel like work, move the admin to Payload 3.0 on the same Cloudflare account. Your D1 schema and R2 image keys carry over with a migration (section 07). You keep the storefront and payments; you gain a maintained editor.
05
Data schema
Verified SQL you can create with CREATE TABLE IF NOT EXISTS on first run. Money is stored as integer minor units (cents) - never floats.
Vehicles and compatibility
Compatibility is a many-to-many between parts and vehicles. Keeping the model list in its own table lets a part match "Vocho" broadly or "Combi Bay window 1968-1979" precisely.
CREATE TABLE vehicles (
id INTEGER PRIMARY KEY AUTOINCREMENT,
slug TEXT UNIQUE NOT NULL, -- 'vocho','combi','brasilia','caribe','safari'
name TEXT NOT NULL, -- 'Vocho'
full_name TEXT, -- 'Volkswagen Tipo 1 (Sedan)'
year_start INTEGER,
year_end INTEGER
);
CREATE TABLE vehicle_variants (
id INTEGER PRIMARY KEY AUTOINCREMENT,
vehicle_id INTEGER NOT NULL REFERENCES vehicles(id) ON DELETE CASCADE,
label TEXT NOT NULL, -- 'Vocho 1500','Combi Bay','Brasilia 1600'
year_start INTEGER,
year_end INTEGER
);Products, parts and condition
CREATE TABLE products (
id INTEGER PRIMARY KEY AUTOINCREMENT,
sku TEXT UNIQUE NOT NULL, -- 'VO-113-501-041-NOS'
part_number TEXT NOT NULL, -- OEM / cross number '113 501 041'
name TEXT NOT NULL,
short_description TEXT,
long_description TEXT,
category TEXT NOT NULL, -- 'suspension','electrical','engine'...
condition TEXT NOT NULL
CHECK (condition IN ('NOS','reproduction','used')),
brand TEXT,
weight_grams INTEGER,
active INTEGER NOT NULL DEFAULT 1,
created_at TEXT NOT NULL DEFAULT (datetime('now'))
);
CREATE INDEX idx_products_condition ON products(condition);
CREATE INDEX idx_products_category ON products(category);
-- a part fits many vehicles/variants
CREATE TABLE product_compat (
product_id INTEGER NOT NULL REFERENCES products(id) ON DELETE CASCADE,
vehicle_id INTEGER NOT NULL REFERENCES vehicles(id) ON DELETE CASCADE,
variant_id INTEGER REFERENCES vehicle_variants(id) ON DELETE CASCADE,
note TEXT, -- 'front left','with ball joints'...
PRIMARY KEY (product_id, vehicle_id, variant_id)
);- SKU is your internal key - stable, never reused. Put the part family, OEM number, and condition into it for human-readable scanning: VO-113-501-041-NOS.
- condition as a check constraint keeps the three states the market actually uses: NOS (new old stock), reproduction, used.
Inventory and reservations
CREATE TABLE inventory (
id INTEGER PRIMARY KEY AUTOINCREMENT,
product_id INTEGER NOT NULL REFERENCES products(id) ON DELETE CASCADE,
location TEXT NOT NULL DEFAULT 'stockroom',
quantity_on_hand INTEGER NOT NULL DEFAULT 0,
quantity_reserved INTEGER NOT NULL DEFAULT 0,
reorder_point INTEGER NOT NULL DEFAULT 0
);
-- A reservation holds stock for an in-flight checkout and auto-expires.
CREATE TABLE reservations (
id INTEGER PRIMARY KEY AUTOINCREMENT,
reserve_key TEXT UNIQUE NOT NULL, -- deterministic idempotency key
order_id INTEGER REFERENCES orders(id),
created_at TEXT NOT NULL DEFAULT (datetime('now')),
expires_at TEXT NOT NULL -- e.g. paid by +15 minutes
);
CREATE TABLE reservation_items (
reservation_id INTEGER NOT NULL REFERENCES reservations(id) ON DELETE CASCADE,
inventory_id INTEGER NOT NULL REFERENCES inventory(id),
quantity INTEGER NOT NULL CHECK (quantity > 0),
PRIMARY KEY (reservation_id, inventory_id)
);available = on_hand - reserved. At checkout you increment quantity_reserved; on payment success you decrement quantity_on_hand and clear the reservation in one transaction; on timeout or failure the reservation rolls back. Section 06 shows the exact transitions.
Images and pricing
CREATE TABLE product_images (
id INTEGER PRIMARY KEY AUTOINCREMENT,
product_id INTEGER NOT NULL REFERENCES products(id) ON DELETE CASCADE,
r2_key TEXT NOT NULL, -- path inside the R2 bucket
alt_text TEXT,
sort_order INTEGER NOT NULL DEFAULT 0,
is_primary INTEGER NOT NULL DEFAULT 0
);
-- Store both currencies; keep the exchange snapshot for reporting only.
CREATE TABLE prices (
id INTEGER PRIMARY KEY AUTOINCREMENT,
product_id INTEGER NOT NULL REFERENCES products(id) ON DELETE CASCADE,
currency TEXT NOT NULL CHECK (currency IN ('MXN','USD')),
amount_minor INTEGER NOT NULL, -- smallest monetary unit, never a float
fx_rate_to_usd REAL, -- snapshot at update time
updated_at TEXT NOT NULL DEFAULT (datetime('now')),
UNIQUE (product_id, currency)
);Display both MXN and USD with the price the owner sets per currency, because auto-fx on the product page can change the buyer's quoted amount. If you prefer one authoritative price, store MXN and show a converted USD badge marked "indicative".
Customers, addresses and shipping
CREATE TABLE customers (
id INTEGER PRIMARY KEY AUTOINCREMENT,
email TEXT NOT NULL,
name TEXT,
phone TEXT,
created_at TEXT NOT NULL DEFAULT (datetime('now'))
);
CREATE TABLE addresses (
id INTEGER PRIMARY KEY AUTOINCREMENT,
customer_id INTEGER NOT NULL REFERENCES customers(id) ON DELETE CASCADE,
kind TEXT NOT NULL CHECK (kind IN ('shipping','billing')),
line1 TEXT NOT NULL,
line2 TEXT,
postal_code TEXT NOT NULL,
city TEXT NOT NULL,
state TEXT,
country TEXT NOT NULL DEFAULT 'MX',
is_default INTEGER NOT NULL DEFAULT 0
);
-- Weight per part drives carrier quotes; store it on products (see 02).
Orders
CREATE TABLE orders (
id INTEGER PRIMARY KEY AUTOINCREMENT,
order_number TEXT UNIQUE NOT NULL, -- friendly, e.g. 'A-1042'
customer_id INTEGER REFERENCES customers(id),
email TEXT NOT NULL,
status TEXT NOT NULL CHECK (status IN (
'pending','payment_processing','paid','reserved','fulfilling',
'shipped','delivered','cancelled','refund_pending','refunded','failed')),
shipping_address_id INTEGER REFERENCES addresses(id),
currency TEXT NOT NULL,
total_minor INTEGER NOT NULL,
created_at TEXT NOT NULL DEFAULT (datetime('now')),
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
);
CREATE INDEX idx_orders_status ON orders(status);
CREATE TABLE order_items (
id INTEGER PRIMARY KEY AUTOINCREMENT,
order_id INTEGER NOT NULL REFERENCES orders(id) ON DELETE CASCADE,
product_id INTEGER NOT NULL REFERENCES products(id),
inventory_id INTEGER REFERENCES inventory(id),
sku TEXT NOT NULL,
name TEXT NOT NULL,
quantity INTEGER NOT NULL CHECK (quantity > 0),
unit_price_minor INTEGER NOT NULL, -- frozen at checkout
currency TEXT NOT NULL
);Payments (append-only ledger)
CREATE TABLE payments (
id INTEGER PRIMARY KEY AUTOINCREMENT,
provider TEXT NOT NULL CHECK (provider IN ('stripe','paypal')),
provider_event_id TEXT, -- Stripe event id / PayPal webhook id
provider_payment_id TEXT NOT NULL, -- Stripe PaymentIntent / PayPal capture
order_id INTEGER NOT NULL REFERENCES orders(id),
amount_minor INTEGER NOT NULL,
currency TEXT NOT NULL,
status TEXT NOT NULL, -- pending/succeeded/failed/refunded/partial
idempotency_key TEXT UNIQUE,
created_at TEXT NOT NULL DEFAULT (datetime('now')),
UNIQUE (provider, provider_event_id)
);06
Checkout & payments
Two providers, one defensive pattern: verify the webhook signature, deduplicate by event id, reserve stock, then commit. Never trust the browser.
The browser should never be allowed to decide that a payment happened. The client only asks the server to create an order and start checkout; the money event reaches your server through a webhook from Stripe or PayPal, which you verify and then act on.
Order creation is idempotent
// Client -> server. Returns the same order on retry.
const res = await createCheckout({ data: {
reserveKey: cartReserveKey, // UUID the client already generated
items, shippingAddress, email
}});
// Server handler
if (alreadyReserved(reserveKey)) return existingOrder(reserveKey);
reserveStock(reserveKey, items, expiresAt = now + 15min); // rows_inv.reserved += qty
return newOrderWithStatus('payment_processing', reserveKey);- reserveKey is a client-generated UUID, stored UNIQUE. A retry returns the same order instead of doubling.
- Stock is reserved now and committed only on a verified, idempotent payment webhook - so the same cart cannot be double-sold while a buyer hesitates.
Stripe webhooks - signature verification
import Stripe from "stripe";
const stripe = new Stripe(env.STRIPE_SECRET_KEY);
// 1. Verify the signature, check timestamp skew, throw on fraud.
let event;
try {
event = stripe.webhooks.constructEvent(
rawBody, sigHeader, env.STRIPE_WEBHOOK_SECRET);
} catch { return respond(400, "invalid signature"); }
// 2. Idempotency: one outcome per (provider, event id).
if (paymentExists(event.id)) return respond(200, "already handled");
// 3. Act on known events.
switch (event.type) {
case "checkout.session.completed":
case "payment_intent.succeeded":
commitPayment(event.id, paymentId, orderId, amount, currency);
break;
case "charge.refunded":
recordRefund(orderId, amount);
break;
}
return respond(200);PayPal webhooks - signature verification
// PayPal sends you a signature to verify against their public key:
// PAYPAL-TRANSMISSION-ID / -SIG / -CERT-URL / -AUTH-ALGO / -TIME, CRYPTO
// 1. Fetch the cert at PAYPAL-TRANSMISSION-CERT-URL, verify CRYPTO signature,
// confirm the certificate resolves to *.paypal.com, check TIME skew.
// 2. Verify the event was NOT already applied (idempotency by event id).
// 3. Then act on event types: CHECKOUT.ORDER.APPROVED /
// PAYMENT.CAPTURE.COMPLETED / PAYMENT.CAPTURE.REFUNDED.
// Simpler and robust: keep the JS SDK "Orders" flow, and at each step
// call the PayPal API back with the webhook id to confirm the state.Because PayPal signed-webhook verification is fiddly (cert fetch, algorithm, time window), the recommended baseline is the PayPal JS SDK Orders v2 flow: the server captures the order and re-verifies against PayPal's API, while the webhook remains the asynchronous source of truth for refunds [9].
Order and payment state transitions
order: pending ─▶ payment_processing ─▶ paid ─▶ fulfilling ─▶ shipped ─▶ delivered
│ │ │
│ └─▶ failed ──────┘ (release reservation, restock)
└─▶ cancelled (timeout) │
└─▶ refund_pending ─▶ refunded (restock if unshipped)
payment: pending ─▶ succeeded ─▶ refunded | partial_refund | failed| Trigger | Order status | Stock effect |
|---|---|---|
| Checkout started | payment_processing | reserved += qty |
| Payment webhook (succeeded) | paid | on_hand -= reserved; reserved = 0 |
| Payment webhook (failed) | failed | release reservation (reserved -= qty) |
| Reservation timeout | cancelled | release reservation |
| Refund webhook (full, unshipped) | refunded | on_hand += qty (restock) |
| Refund webhook (full, shipped) | refunded | no restock; carrier return handled manually |
| Partial refund | paid | no stock change |
Refunds and failure handling
- Refunds come in through the same signed webhooks (charge.refunded, PAYMENT.CAPTURE.REFUNDED), recorded against the original payment row with the amount.
- Restock a full refund only if the order never shipped; a shipped part coming back is a manual return, not an automatic restock.
- On webhook failure or timeouts, the reservation expiry frees stock automatically. Add a daily reconciliation that flags payment_processing orders older than 24h so you never hold money with no record.
07
Security, speed & operations
The non-functional requirements that decide whether the store survives its first busy month.
Security
- Secrets live in Worker secrets / bindings, never in the bundle or repo. The STRIPE_WEBHOOK_SECRET, Stripe and PayPal keys stay server-side.
- Cloudflare Access fronts /admin; add Workers rate limiting on checkout and order-creation endpoints.
- Reject tampered prices: compute totals server-side from your D1 prices, never trust client-submitted amounts. Validate all input, bind ORDER BY columns, and use parameterized queries everywhere.
- Store only the last four digits (or nothing) of any card data - the token lives with the processor. Logging should never capture card numbers, signatures, or secrets.
Performance
- Static assets are free and unlimited on Workers; lean on the CDN cache for images and HTML [1].
- Add a single index on every column you filter or sort (category, condition, status, part_number) to keep D1 rows-read low and fast.
- Preload above-fold images with sized attributes to prevent layout shift; lazy-load the rest. Keep product images under ~25 MiB (the single-asset cap) and typically far below that.
Accessibility and mobile for the owner
- Build the admin for a phone first: 44px hit targets, 16px inputs, plain Spanish labels, a big "Add a part" button, and contrast that passes for older eyes.
- Use semantic HTML, real buttons and links, visible focus rings, and descriptive alt text on product images. Respect prefers-reduced-motion.
SEO
- Since it is server-rendered static/TanStack output, each product page gets a unique title, meta description, canonical URL and Open Graph image. Schema.org Product markup with price and availability helps rich results.
- Use stable, human-readable URLs per part (/autopartas/vocho-113-501-041-nos) and a sitemap submitted to the vendor search consoles.
Backups and observability
- D1 exports to SQL on a schedule (a Cloudflare Cron Trigger to R2); wrangler d1 export gives you a portable file. Test restoring it before you rely on it, and keep R2 image keys in the database so the pairing survives.
- Use Workers Logs or Logpush to an R2 bucket and set alerts on failed webhooks, cancelled payment_processing orders, and low stock at reorder points.
Migration and a phased launch
| Phase | Scope | Exit check |
|---|---|---|
| 0. Foundation | Cloudflare account, Pages/Worker, D1 schema, R2 bucket, Access on /admin | Admin can create a part and see it in R2 |
| 1. Catalog | Seed vehicles + variants, import first part photos and SKUs | Storefront lists and filters real parts |
| 2. Checkout | Stripe + PayPal, signed webhooks, reservations, order screens | A real test order lands in D1 and updates stock |
| 3. Soft launch | Limited catalog, you ship manually, watch webhook + refund flows | 10+ successful real orders with zero double-sales |
| 4. Full launch | SEO, sitemap, analytics, backup cron, owner training | Owner can run daily operations from a phone |
Migration off this stack is deliberately easy: D1 is SQLite (portable to Postgres/MySQL), R2 speaks the S3 API, and your data model is plain relational SQL. Nothing vendor-locks your catalog.
08
Cost & zero-cost tradeoffs
Verified, published figures. Fees change; the links in section 10 are the source of truth.
| Item | Free tier | Beyond free | Status |
|---|---|---|---|
| Cloudflare Workers | 100,000 requests/day; static assets unlimited | $5/mo floor on Paid, then 10M req/mo + $0.30/million | Verified |
| Cloudflare D1 | 5M rows read + 100k rows written/day; 5 GB; scale-to-zero | 25B reads + $0.001/M; 50M writes + $1.00/M; $0.75/GB-mo | Verified |
| Cloudflare R2 | 10 GB-month; 1M class A + 10M class B ops; free egress | $0.015/GB-mo standard; $4.50/M class A; $0.36/M class B | Verified |
| Cloudflare Pages | 500 builds/mo; 20,000 files; 25 MiB asset cap | Raise with a plan | Verified |
| Cloudflare Access | Zero Trust Free up to 50 users | Paid per-seat - confirm current price at signup | Verified |
| Domain name | - | ~USD 10-12/yr for a .com or .mx via Cloudflare registrar | Assumption |
| Stripe (Mexico) | - | 3.6% + MXN 3.00 national cards; +0.5% intl; +2% currency conv; MXN 150 dispute | Verified |
| PayPal (Mexico) | - | 3.95% + MXN 4.00 national; tiered down to 2.95%; +0.5% intl; MXN 110 chargeback | Verified |
The realistic launch budget is a domain plus payment fees. Payment processing is percentage-based, so it scales with sales, not with uptime - the right shape for a business that is just starting.
Zero-cost tradeoffs
- Transaction fees are not zero: Stripe and PayPal take ~3.6-3.95% + a fixed amount per national card [6][7]. There is no free way around processor fees except offline/pickup payment, which you lose by being online-first.
- Email and shipping are not free: transactional email (order confirmations) and real shipping labels are small but real costs. Include them in unit economics; the Cloudflare free tier does not cover them.
- Free-tier limits are real: the Workers free plan caps D1 writes at 100,000 rows/day - plenty, but not infinite. The moment you need reliable headroom, the Paid plan's $5/month floor is the honest minimum.
- Self-hosting a CMS costs more than it saves: a "free" Node/Postgres CMS needs a server you patch and monitor - time and risk that a $0 serverless stack avoids.
09
Working rules (AGENTS.md)
A complete, project-tailored AGENTS.md to hand the next build agent or contractor. Copy it into the repo root before real implementation starts.
# AGENTS.md - Vocho Autoparts Store
You are the build agent for "Vocho Autoparts", an e-commerce site that sells
classic VW parts (Vocho, Combi, Brasilia, Caribe, Safari) in Mexico, for a
nontechnical owner. This is a real store, not a demo. Read this file, then
build to it exactly.
## Non-negotiable rules
1. NEVER HALLUCINATE DATA OR CODE
- Do not invent product names, prices, SKUs, OEM part numbers, fitment,
customer records, or review text. Every product and order is real. If
you need data you do not have, use clearly-marked sample/seed data and
say so; never present it as fact.
- Do not fabricate API endpoints, config values, environment variables,
tokens, or dependencies. Only use what is installed in package.json or
explicitly added by you in this repo.
2. NEVER FABRICATE APIS OR CONFIG
- Stripe and PayPal calls must match their published API. If you cannot
verify an endpoint with the vendor's own docs, do not ship it.
- Do not invent env var names; define them in exactly one place and read
them only server-side. Never leak secrets to the browser or the repo.
3. VERIFY BEFORE YOU DEPEND
- Adding a dependency? Confirm it exists, is maintained, and actually
installs. Unverified dependencies are forbidden by default.
4. NO SLOPPY CODE
- Typecheck and lint the changed files before you finish. Keep the build
passing. No copy-paste TODOs, no dead code, no unused imports.
5. PAYMENTS ARE SECURE OR THE STORE DOES NOT LAUNCH
- Verify webhook signatures (Stripe constructEvent; PayPal cert/signature).
- Make order creation and payment handling idempotent.
- Never trust the browser for prices or payment results.
- Keep secrets server-side. Never log card data.
6. READ THE REPO FIRST
- Read __root.tsx, index.tsx, package.json, and the schema files before
writing anything. Match existing conventions.
7. SCHEMA AND MIGRATIONS
- All D1 tables use CREATE TABLE IF NOT EXISTS and are idempotent.
- Money is integer minor units, never floats.
- Schema changes ship with a migration, never an ad-hoc alter in prod.
8. BUILD, THEN VERIFY
- Run typecheck, lint, tests, and the production build; fix every error.
- Load the built site and check every route you changed renders real
content, not an error boundary.
9. NO SECRETS IN THE REPO
- Keys go in Worker secrets / bindings. .env.example documents names only.
10. CITE CLAIMS
- Any technical or pricing claim you add carries a link to a real,
checkable source. Mark assumptions as assumptions.
11. STOP AND ASK WHEN BLOCKED
- If an unknown blocks correct work (an unclear requirement, a missing
credential, an assumption that changes the design), stop and ask. Do
not guess around a decision that matters.
## Stack (fixed)
- Cloudflare: Pages/Worker + D1 (SQLite) + R2 (images) + Cloudflare Access
on /admin.
- Payments: Stripe + PayPal, signed webhooks, reservations.
- Framework: as already in this repo (React + TanStack + Tailwind). Do not
swap it.
- Admin: custom D1/R2 screens, mobile-first, plain Spanish, 44px targets.
## Definition of done
Build passes, routes render, webhook signature verification works against
test keys, a real test checkout updates stock and orders exactly once, and
no invented product or config is shipped.10
Sources & verification
Pricing and technical claims point at the vendor's own pages. The verified figures above were read from these URLs during the build; fees change, so re-check before launch.
- 01Cloudflare Workers pricing
- 02Cloudflare D1 pricing
- 03Cloudflare R2 pricing
- 04Cloudflare Pages limits
- 05Cloudflare Workers free-plan limits
- 06Stripe pricing - Mexico
- 07PayPal business fees - Mexico
- 08Stripe webhook signatures
- 09PayPal webhooks
- 10Strapi
- 11Directus
- 12Payload CMS 3.0
- 13Decap CMS
- 14TinaCMS
- 15Cloudflare D1 documentation
- 16Cloudflare Access / Zero Trust