Vocho Autoparts

Cloudflare architecture blueprint

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.

Claim tags:Verified Recommendation Assumption
5vehicle platforms
1database, scale-to-zero
2payment providers
$0hosting base (free tier)

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.

textarch / system diagram
                         ┌────────────────────────────────────────────┐
                         │            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.

OptionModelCloudflare fitReal cost caveatMobile for a nontechnical ownerRead
StrapiSelf-hosted headless CMSRuns on a Node/PG server; needs a VPS or managed Postgres - not native to WorkersAssumptionNeeds a server + database you operate (real money and upkeep)Heavy to self-host
DirectusSelf-hosted headless + data studio over SQLSelf-host on VPS; data stays in your own SQLAdmin can wrap D1, but the usual path is a Node server + Postgres/MySQLAssumptionCapable but complex
Payload 3.0TypeScript CMS that runs in a WorkerFirst-class Workers support; deploys with your storeFree/open core; Cloudflare paid plan floor may apply for prodRecommendationGood middle ground
Decap CMSGit-based, static CMSExcellent (file-based, no server)Needs Netlify Identity or OAuth; content is files, not relationalClunky for parts inventory; fine for a blog, weak for SKU dataAssumption
TinaCMSGit-based visual editorGood (files + API)Open core; cloud hosting is paid, self-host is DIYVisual but inventory + reservations need custom fields either wayAssumption
Custom D1/R2 adminSmall custom app you ownNative - just another Worker screenNear zero at launch; it is your code to maintainRecommendationBest 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.

sqlsql / 01_vehicles.sql
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

sqlsql / 02_products.sql
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

sqlsql / 03_inventory.sql
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

sqlsql / 04_images_pricing.sql
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

sqlsql / 05_customers.sql
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

sqlsql / 06_orders.sql
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)

sqlsql / 07_payments.sql
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

tsts / create-order.ts
// 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

tsts / stripe-webhook.ts
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

tsts / paypal-webhook.ts
// 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

texttext / state machine
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
TriggerOrder statusStock effect
Checkout startedpayment_processingreserved += qty
Payment webhook (succeeded)paidon_hand -= reserved; reserved = 0
Payment webhook (failed)failedrelease reservation (reserved -= qty)
Reservation timeoutcancelledrelease reservation
Refund webhook (full, unshipped)refundedon_hand += qty (restock)
Refund webhook (full, shipped)refundedno restock; carrier return handled manually
Partial refundpaidno 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

PhaseScopeExit check
0. FoundationCloudflare account, Pages/Worker, D1 schema, R2 bucket, Access on /adminAdmin can create a part and see it in R2
1. CatalogSeed vehicles + variants, import first part photos and SKUsStorefront lists and filters real parts
2. CheckoutStripe + PayPal, signed webhooks, reservations, order screensA real test order lands in D1 and updates stock
3. Soft launchLimited catalog, you ship manually, watch webhook + refund flows10+ successful real orders with zero double-sales
4. Full launchSEO, sitemap, analytics, backup cron, owner trainingOwner 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.

ItemFree tierBeyond freeStatus
Cloudflare Workers100,000 requests/day; static assets unlimited$5/mo floor on Paid, then 10M req/mo + $0.30/millionVerified
Cloudflare D15M rows read + 100k rows written/day; 5 GB; scale-to-zero25B reads + $0.001/M; 50M writes + $1.00/M; $0.75/GB-moVerified
Cloudflare R210 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 BVerified
Cloudflare Pages500 builds/mo; 20,000 files; 25 MiB asset capRaise with a planVerified
Cloudflare AccessZero Trust Free up to 50 usersPaid per-seat - confirm current price at signupVerified
Domain name-~USD 10-12/yr for a .com or .mx via Cloudflare registrarAssumption
Stripe (Mexico)-3.6% + MXN 3.00 national cards; +0.5% intl; +2% currency conv; MXN 150 disputeVerified
PayPal (Mexico)-3.95% + MXN 4.00 national; tiered down to 2.95%; +0.5% intl; MXN 110 chargebackVerified

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.

markdownAGENTS.md
# 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.

  1. 01Cloudflare Workers pricing
  2. 02Cloudflare D1 pricing
  3. 03Cloudflare R2 pricing
  4. 04Cloudflare Pages limits
  5. 05Cloudflare Workers free-plan limits
  6. 06Stripe pricing - Mexico
  7. 07PayPal business fees - Mexico
  8. 08Stripe webhook signatures
  9. 09PayPal webhooks
  10. 10Strapi
  11. 11Directus
  12. 12Payload CMS 3.0
  13. 13Decap CMS
  14. 14TinaCMS
  15. 15Cloudflare D1 documentation
  16. 16Cloudflare Access / Zero Trust