1. Payment Processing
Every site that sells products uses Stripe Checkout (hosted) as the payment gateway. This is a non-negotiable architectural decision. We never handle card data on our servers.
Why Stripe Checkout (Hosted)
Stripe Checkout redirects the customer to a Stripe-hosted payment page. The card number, CVV, and expiration never touch our server, our database, or our code. This matters for three reasons:
- PCI SAQ-A compliance — the simplest Self-Assessment Questionnaire. No penetration testing, no quarterly scans of our payment forms, no encrypted card storage. We qualify because card data never enters our environment.
- Built-in fraud detection — Stripe Radar analyzes billions of transactions to block fraudulent cards before charges are attempted. We get enterprise-grade fraud prevention without building anything.
- No card data liability — if our server is compromised, no card numbers are exposed because none were ever stored or transmitted through our infrastructure.
API Key Management
Stripe uses two key pairs — one for testing, one for live transactions. Both must be stored as environment variables, never in source code.
| Variable | Environment | Purpose |
|---|---|---|
STRIPE_SECRET_KEY |
Server-side only | Creates checkout sessions, verifies webhooks, manages orders. Starts with sk_test_ or sk_live_ |
STRIPE_PUBLIC_KEY |
Client-side (safe to expose) | Initializes Stripe.js on the frontend. Starts with pk_test_ or pk_live_ |
STRIPE_WEBHOOK_SECRET |
Server-side only | Verifies that incoming webhook events are genuinely from Stripe. Starts with whsec_ |
.env files (added to .gitignore). On production, the .env lives in the shared/ directory and persists across deployments. Test keys go in your DevHub .env, live keys go in production .env.
Checkout Flow
The full lifecycle from cart to confirmed order:
1. Cart Page (Browser)
2. Create Checkout Session (Our API)
3. Redirect to Stripe (Browser)
4. Webhook Callback (Stripe → Our API)
5. Order Confirmed
Webhook Signature Verification
Webhooks are the authoritative signal that payment succeeded. The success URL redirect is not trustworthy — a user could visit that URL directly. Always verify webhook signatures before processing orders.
$payload = file_get_contents('php://input');
$sig_header = $_SERVER['HTTP_STRIPE_SIGNATURE'];
$secret = getenv('STRIPE_WEBHOOK_SECRET');
// Extract timestamp and signature from header
$elements = explode(',', $sig_header);
// Parse t= and v1= values
$timestamp = ...; // from t= element
$signature = ...; // from v1= element
// Compute expected signature
$signed_payload = $timestamp . '.' . $payload;
$expected = hash_hmac('sha256', $signed_payload, $secret);
// Constant-time comparison prevents timing attacks
if (!hash_equals($expected, $signature)) {
// Reject stale events (older than 5 minutes)
if (abs(time() - $timestamp) > 300) {
Required Webhook Events
| Event | When It Fires | What We Do |
|---|---|---|
checkout.session.completed |
Payment succeeded and session is complete | Create order record, decrement inventory, send confirmation email |
checkout.session.expired |
Customer abandoned checkout (session timed out after 24 hours) | Log the abandonment, optionally send recovery email if customer email was collected |
2. Form Handling
Every site has forms — contact forms, quote requests, product inquiries. All forms follow the same server-side handler pattern regardless of what the form collects.
The Handler Pipeline
Every form submission passes through four stages in exactly this order:
2. SANITIZE
3. STORE
4. NOTIFY
CSRF Token Generation
Every form must include a CSRF token to prevent cross-site request forgery. The token is generated server-side, embedded in the form as a hidden field, and verified on submission.
if (empty($_SESSION['csrf_token'])) {
// Embed in form
<input type="hidden" name="csrf_token"
// Verify on submission (constant-time comparison)
if (!hash_equals($_SESSION['csrf_token'], $_POST['csrf_token'])) {
hash_equals() runs in constant time regardless of where strings differ.
Rate Limiting
Prevent form abuse by limiting submissions per IP address within a time window.
- Default limit: 5 submissions per IP per 60 seconds
- Implementation: Store submission timestamps per IP in the database or in-memory (Redis if available)
- Response: Return HTTP 429 (Too Many Requests) with a
Retry-Afterheader - Do not reveal the exact limit in error messages — simply say "Please wait before submitting again"
Honeypot Spam Prevention
A honeypot is a hidden form field that real users never see or fill in, but bots fill automatically. If the field has a value on submission, the request is spam.
<div style="position:absolute;left:-9999px;" aria-hidden="true">
// Server-side check
if (!empty($_POST['website_url'])) {
Email Notification Template
Every form submission triggers an HTML email to the site owner. The email must be clear, scannable, and include all submitted data.
- Subject line:
[Site Name] New {Form Type} from {Name} - Body: Clean HTML table with field labels and values
- Footer: Submission timestamp, IP address, user agent
- Reply-To: Set to the customer's email address so the owner can reply directly
Input Validation Summary
| Field Type | Validation | Sanitization |
|---|---|---|
| Name | Required, 2–100 characters | htmlspecialchars(), trim() |
Required, filter_var(FILTER_VALIDATE_EMAIL) |
filter_var(FILTER_SANITIZE_EMAIL) |
|
| Phone | Optional, 7–20 characters, digits/spaces/dashes/parens only | Strip non-numeric for storage, keep formatted for display |
| Message / Textarea | Required, 10–5000 characters | htmlspecialchars(), trim(), preserve newlines |
| Select / Radio | Value must be in allowed list (whitelist check) | Cast to expected type |
required and pattern attributes improve UX but are trivially bypassed. Every validation check must be duplicated server-side. Client-side is for convenience; server-side is for security.
3. Product Data Feeds
Product data feeds allow your products to appear in Google Shopping, Facebook/Instagram shops, and other marketplaces. The feed is a structured file generated from your product database and served at a public URL.
Google Merchant Center Feed
Google Shopping uses an RSS 2.0 feed with the g: namespace for Google-specific fields. The feed must be served as XML at a stable URL.
Required Fields
| Field | Description | Example |
|---|---|---|
g:id |
Unique product identifier (your internal SKU) | PC-HD4500-BLK |
g:title |
Product name (max 150 chars) | HD-4500 Custom Full Chassis - Black |
g:description |
Product description (max 5000 chars, no HTML) | Plain text description of the product |
g:link |
Product page URL (canonical, absolute) | https://patriotchassis.com/products/hd-4500 |
g:image_link |
Primary product image URL (min 100x100px) | https://patriotchassis.com/images/hd4500-front.jpg |
g:price |
Price with currency code | 24999.00 USD |
g:availability |
Stock status | in_stock, out_of_stock, or preorder |
g:condition |
Product condition | new |
g:brand |
Brand name | Patriot Chassis |
Feed Structure
<rss version="2.0" xmlns:g="http://base.google.com/ns/1.0">
<channel>
</rss>
<g:identifier_exists>false</g:identifier_exists> to tell Google not to expect them. This prevents feed disapprovals.
Facebook / Instagram Feed
Facebook Commerce Manager accepts a CSV file with these columns:
| Column | Required | Notes |
|---|---|---|
id |
Yes | Same SKU as Google feed for consistency |
title |
Yes | Product name |
description |
Yes | Plain text, max 9999 chars |
availability |
Yes | in stock or out of stock (space, not underscore) |
condition |
Yes | new, refurbished, or used |
price |
Yes | 24999.00 USD |
link |
Yes | Product page URL |
image_link |
Yes | Product image URL (min 500x500px recommended) |
brand |
Yes | Brand name |
Feed Requirements
- Only include products with fixed prices and active status. "Call for price" or "Build to order" items with no set price should be excluded from feeds — both Google and Facebook will reject them.
- Prices must match the landing page. If the feed says $24,999 but the product page says "Starting at $22,000," the listing will be disapproved.
- Images must be real product photos, not placeholder graphics or text-only images.
- URLs must be canonical and HTTPS. No redirects, no tracking parameters in the feed URL.
Feed Refresh Schedule
- Automated generation: Every 6 hours via cron job or scheduled task
- Manual trigger: Admin dashboard button to regenerate immediately after price changes or new products
- Feed URL:
https://example.com/feeds/google-products.xmlandhttps://example.com/feeds/facebook-products.csv
Validation
- Google: Use the Merchant Center feed diagnostics to check for errors and warnings after each upload
- Facebook: Use the Commerce Manager catalog diagnostics to verify all items are approved
- Before submitting: Validate XML structure and required field presence locally
4. Product Data Architecture
Product data flows through a three-tier architecture. Understanding this hierarchy is mandatory before writing any code that reads, writes, syncs, or deploys product data.
The Three Tiers
| Tier | Name | Database | Purpose |
|---|---|---|---|
| 1. Portal | Product Pipeline Portal | portal_products + portal_product_images + portal_product_specs + related tables |
Where product data is born and enriched. Full editorial workflow with 13 section tabs, image management, specs, videos, documentation, marketing, and manufacturing data. Multi-site: every product belongs to a site. |
| 2. THR Hub | The High Road Hub | thr_products |
The central distribution database. Receives synced data from the Portal. Serves as the single source of truth for client-facing product data, manufacturing status, and channel distribution. Client portal reads directly from this table. |
| 3. Site DB | Per-site product tables | e.g. pc_products, ths_products |
Each website has its own independent product table. Receives data via deploy from THR Hub. The site reads only from its own table. This isolation ensures any site can operate as a standalone platform if separated from the Hub in the future. |
Data Flow Model
portal_product_images are compiled into a JSON array on thr_products.images. Sync is bidirectional for field corrections but the Portal is authoritative.
thr_products for a given client, writes them into the destination site's product table, copies image files to the site directory, and regenerates products-data.js and feed files.
products-data.js. It never queries thr_products or portal_products at runtime. This means any site can be detached from the Hub and operate independently.
portal_product_images are serialized into the thr_products.images JSON column. When deploying THR Hub → Site, image files are copied from the source site directory to the destination site directory, preserving subdirectory structure.
Key Tables
| Table | Tier | Prefix | Notes |
|---|---|---|---|
portal_products | Portal | portal_ | Canonical product record. site_id links to portal_sites. |
portal_product_images | Portal | portal_ | Normalized image table with metadata (alt, title, role, dimensions, status). |
portal_product_specs | Portal | portal_ | Structured specs (group, key, value, unit). |
thr_products | THR Hub | thr_ | Hub distribution table. client_id links to thr_clients. Images and specs stored as JSON columns. |
pc_products | Site DB | pc_ | Patriot Chassis website product table. |
ths_products | Site DB | ths_ | The High Road website product table (THR Web). |
Site-to-Client Mapping
The portal_sites table links the Portal tier to the THR Hub tier. Key columns:
thr_client_id— links this site to a THR Hub client (source for deploy)serves_via_thr— marks this site as a deploy destinationdb_name,db_prefix,site_dir— site infrastructure for deploy
A site can be a source (has thr_client_id), a destination (has serves_via_thr=1 with infrastructure), or both.
What Gets Synced vs. Deployed
| Data | Portal → THR Hub (Sync) | THR Hub → Site DB (Deploy) |
|---|---|---|
| 31 product fields | Yes (SYNC_FIELD_MAP) | Yes |
| Images | Yes (portal_product_images → JSON) | Yes (JSON → site table + file copy) |
| Specs | No (deploy only) | Yes (JSON) |
| Image files | N/A (paths only) | Yes (copied to site directory) |
| products-data.js | N/A | Regenerated |
| Feed files | N/A | Regenerated |
Separation Principle
The THR Hub and each Site DB are designed to be separable. If a client outgrows the Hub or needs an independent platform, their site already has its own product table, its own image directory, its own products-data.js, and its own feed files. Detaching requires only removing the deploy step — the site continues to function with its local data.
This is why every site must have its own product table with its own prefix, rather than querying the Hub at runtime. The Hub is a distribution mechanism, not a runtime dependency.
Implementation Reference — Dual-Prefix Configuration
Every site that participates in the three-tier architecture uses two database prefixes within the same database. This is the most commonly misunderstood aspect of the system. Both prefix constants must be defined in the site’s .env file and loaded in api/config.php.
| Constant | Value | Purpose | Used By |
|---|---|---|---|
DB_PREFIX |
thr_ |
THR Hub tables — administrative catalog, client management, sync tracking | Admin APIs only (/api/admin/*) |
SITE_PREFIX |
ths_ |
Site DB tables — public-facing product data served to visitors | Website APIs (public-products.php, client-products.php, orders.php, generate-products-js.php) |
thr_*) and the Site DB tables (ths_*) live side-by-side. If only the thr_ tables are created (e.g., by running only the Hub migration), the website will fail because it queries ths_products which does not exist.
Production .env Template
How config.php Loads the Prefixes
API Routing Rules — Which Table, Which Prefix
This table is the single reference for which API endpoints read from which database tier. Getting this wrong is the #1 cause of deployment failures.
| API File | Reads From | Writes To | Prefix | Context |
|---|---|---|---|---|
public-products.php |
ths_products |
— | SITE_PREFIX | Public shop pages |
client-products.php |
ths_products |
ths_products + backpush to thr_products |
SITE_PREFIX (read) + DB_PREFIX (backpush) | Client portal |
orders.php |
ths_products |
thr_orders |
SITE_PREFIX (products) + DB_PREFIX (orders) | Checkout & order history |
generate-products-js.php |
ths_products + variant tables |
Writes js/products-data.js file |
SITE_PREFIX | JS data regeneration |
admin/products.php |
thr_products |
thr_products |
DB_PREFIX | Admin product management |
admin/push.php |
thr_products |
ths_products |
DB_PREFIX → SITE_PREFIX | Hub-to-Site deploy |
admin/pull.php |
ths_products |
thr_products |
SITE_PREFIX → DB_PREFIX | Site-to-Hub sync |
thr_products in website-facing code. If a public page, client portal page, order flow, or products-data.js generator queries thr_products instead of ths_products, it is a bug. The Hub is for admin management only. The website serves from the Site DB.
Site DB Table Schema
These tables must exist on the production database alongside the thr_ Hub tables. They are the tables the live website reads from. If they are missing, the site will return empty product listings or database errors.
ths_products — Site Product Table
ths_product_variant_groups — Variant Selectors
ths_product_variant_options — Variant Values
Deployment Implementation — How Data Moves
This section documents the concrete steps that move product data from the Hub tier to the Site tier. The architectural rules above are meaningless if the deployment is not executed correctly.
Step-by-Step: Hub → Site DB Deploy
- Run all migrations — Execute every
.sqlfile inmigrations/in order. This creates both thethr_Hub tables and theths_Site tables. If the site tables migration is missing, create one before proceeding. - Verify both table sets exist — Query
SHOW TABLES LIKE 'thr_%'andSHOW TABLES LIKE 'ths_%'. You must see tables in both result sets. - Populate Hub tables — Insert or import product data into
thr_products,thr_clients, andthr_settings. This can come from a SQL dump, admin UI, or Portal sync. - Push data to Site tables — Use
/api/admin/push.php?action=push-all&site_id=N(or equivalent script) to copy products fromthr_productstoths_products. This maps fields, copies images, and records the sync. - Sync variant data — Variant groups and options must be populated in
ths_product_variant_groupsandths_product_variant_options. The push mechanism handles this, or use the Portal deploy. - Regenerate products-data.js — Run
php api/generate-products-js.php(or call the endpoint). This readsths_products+ variant tables and writesjs/products-data.js. Without this file, the shop page shows no products. - Verify — Hit
/api/public-products.php?action=list. If it returns products, the Site DB is correctly populated. If it returns an empty array or error,ths_productsis missing or empty.
ths_products empty while thr_products has all the data. The website will appear to have no products.
Backpush: Site DB → Hub
Certain website actions write to the Site DB first, then “backpush” the change to the Hub to keep both tiers synchronized. This happens in client-products.php when a client toggles public listing or distribution channels:
The backpush uses slug matching (not ID) because the product’s ID may differ between the Hub and Site tables. The slug is the stable cross-tier identifier.
Common Deployment Failures
| Symptom | Cause | Fix |
|---|---|---|
| Shop page shows no products | ths_products table is empty or doesn’t exist |
Run site tables migration, then push data from thr_products |
| API returns “Table doesn’t exist” | Only thr_ tables were created; ths_ migration was skipped |
Run the site tables migration (008-site-tables.sql) |
| Admin shows products but website doesn’t | Data is in thr_products (Hub) but never pushed to ths_products (Site) |
Run the push step: push.php?action=push-all |
| Products show but no variant selectors | ths_product_variant_groups and ths_product_variant_options are empty |
Sync variants from Portal or populate directly |
| Client toggles don’t persist after re-deploy | Backpush to Hub failed; re-deploy overwrote site changes | Verify backpush is working; check for slug mismatches |
5. Admin Dashboard Patterns
Every commerce site needs an admin interface for managing orders, products, and submissions. The admin follows a consistent pattern across all builds.
Authentication
Admin authentication uses a simple shared token model. No user accounts, no session management, no password hashing complexity.
- Token storage: A single
ADMIN_TOKENin the site'ssettings.phpor.envfile - Login flow: Admin enters the token on a login page. If it matches, store it in
sessionStorage. - API auth: Every admin API request includes the token as a
Bearerheader:Authorization: Bearer {token} - Verification: Server checks the token with
hash_equals()on every request
$token = str_replace('Bearer ', '', $_SERVER['HTTP_AUTHORIZATION'] ?? '');
$valid = hash_equals($settings['admin_token'], $token);
if (!$valid) {
UI Patterns
The admin dashboard uses the same dark theme as the Hub and Binding Protocol. Consistent styling reduces cognitive switching.
Stats Cards
Top of dashboard shows aggregate numbers at a glance:
- Total Orders — all-time count
- Revenue — sum of paid orders
- Pending Orders — orders awaiting action
- Products — total active product count
Data Tables
- Sortable columns (click header to sort)
- Expandable rows for detail views (click row to expand)
- Color-coded status badges: pending, paid, processing, shipped, cancelled
- Inline editing for prices and inventory (click to edit, Enter to save)
Status Management
- Dropdown selectors to change order status
- Status change triggers appropriate action (e.g., changing to "shipped" prompts for tracking number)
- All status changes logged with timestamp
sessionStorage (cleared when the browser tab closes). No cookies, no server-side sessions, no refresh tokens. If the tab is closed, log in again. Simple and secure.
6. Order Management Lifecycle
Every order follows a defined status flow from creation to completion. The statuses, transitions, and rules are the same across all sites.
Status Flow
| Status | Meaning | Transitions To |
|---|---|---|
| pending | Checkout session created, awaiting payment | paid, cancelled |
| paid | Payment confirmed via webhook | processing, cancelled, refunded |
| processing | Order being prepared / built / assembled | shipped, refunded |
| shipped | Shipped with tracking number provided | delivered |
| delivered | Confirmed received by customer (terminal state) | — |
| cancelled | Order cancelled before shipment (terminal state) | — |
| refunded | Payment returned to customer (terminal state) | — |
Order Numbers
- Format: Sequential with site prefix — e.g.,
PC-000001,PC-000002 - Prefix: Two-letter abbreviation unique to each site (PC = Patriot Chassis, AO = Advanced Offroad, etc.)
- Zero-padded: 6 digits minimum for consistent display and sorting
- Never reused: Cancelled orders keep their number. The next order gets the next number.
Inventory Tracking
checkout.session.completed fires and payment is confirmed. If a product sells out between cart add and payment, the webhook handler should flag the order for manual review.
Shipping Tracking
- Carrier: Dropdown selection (USPS, UPS, FedEx, Freight/LTL, Other)
- Tracking number: Manual entry by admin when status changes to "shipped"
- Customer notification: Email sent automatically with tracking number and carrier link
- Tracking URL pattern: Auto-generate based on carrier (e.g.,
https://tools.usps.com/go/TrackConfirmAction?tLabels={number})
Email Notifications
| Trigger | Recipient | Content |
|---|---|---|
| Payment confirmed | Customer + site owner | Order confirmation with items, total, order number |
| Status changed to shipped | Customer | Shipping notification with tracking number and link |
| Status changed to cancelled | Customer | Cancellation notice |
| Status changed to refunded | Customer | Refund confirmation with expected timeline |
7. Customer Accounts
Customer accounts add significant complexity — password storage, reset flows, session management, profile pages, and data privacy obligations. For most small-business e-commerce sites, this complexity is not justified at launch.
Phased Approach
MVP (Launch) — No Accounts
- Guest checkout only. Customer provides name, email, phone, and shipping address during checkout. No account creation.
- Order lookup: A simple "Check Order Status" page where the customer enters their email address and order number. If both match, show the order status and tracking info.
- No passwords, no sessions, no data retention concerns beyond order fulfillment.
Phase 2 (Future) — Customer Accounts
- Optional account creation after order completion ("Save your info for next time")
- Order history dashboard
- Saved addresses for faster checkout
- Password hashing with
password_hash()(bcrypt, cost 12) - Email-based password reset with time-limited tokens
Phase 3 (Future) — Dealer / Wholesale Tiers
- Dealer application and approval workflow
- Tiered pricing: retail, dealer, wholesale
- Price visibility rules (dealers see discounted prices after login)
- Minimum order quantities for wholesale
- Net-30 / Net-60 payment terms for approved dealers
8. Privacy & Legal Requirements
Every commerce site needs legal pages. These are not optional — Stripe requires a privacy policy, and selling online without terms of service exposes the business to unnecessary risk.
Privacy Policy
The privacy policy must cover:
- What data is collected: Name, email, phone, shipping address, order history, IP address, browser info
- How data is used: Order fulfillment, shipping, customer communication, fraud prevention
- Payment processor disclosure: Explicitly state that payment is processed by Stripe, that card data is never stored on our servers, and link to Stripe's privacy policy
- Data sharing: We do not sell, rent, or share personal information with third parties except as required for order fulfillment (shipping carriers) or legal compliance
- Data retention: Order records retained for business/tax purposes. Contact form submissions retained for the stated purpose, then deleted
- Contact information: How to request data deletion or ask questions about privacy
Terms of Service
The terms of service must cover:
- Pricing: All prices in USD. Prices subject to change without notice. Errors in pricing may be corrected.
- Orders: Placing an order constitutes an offer to purchase. We reserve the right to refuse or cancel orders.
- Custom / build-to-order products: Lead times, non-refundable deposits, change order policies
- Returns & exchanges: Policy for stock items vs. custom orders. Timeframe, condition requirements, restocking fees if any.
- Warranty: What is covered, duration, exclusions (modifications, misuse, wear items)
- Limitation of liability: Products used at buyer's own risk. Maximum liability limited to purchase price.
- Governing law: State and jurisdiction for dispute resolution
Cookie Consent
- If you add Google Analytics, Facebook Pixel, or any tracking: a cookie consent banner becomes mandatory
- If you are EU-facing: consent must be opt-in (no pre-checked boxes)
- Document all cookies in your privacy policy regardless
Stripe Merchant Requirements
- A publicly accessible privacy policy is required to activate your Stripe account
- The privacy policy must mention that Stripe processes payments
- Your website must clearly display: business name, product descriptions, pricing, refund/return policy, and contact information
Footer Links
components.js footer. The links must be present on every page, not just the checkout.
Content Guidelines
- Write in plain language. Short sentences, clear structure, numbered sections. No legalese walls.
- Use headings and bullet points so customers can find what they need without reading the entire document
- Include a "Last Updated" date at the top of each legal page
- State the obvious. "We do not sell your personal information" is worth saying explicitly, even if it seems obvious.
9. Security Checklist
This checklist covers every security measure required before a commerce site goes live. Every item must pass. No exceptions.
| Status | Security Measure | Why It Matters |
|---|---|---|
| ✓ | CSRF tokens on all forms | Prevents attackers from submitting forms on behalf of other users via malicious links or embedded pages |
| ✓ | Parameterized SQL queries (PDO prepared statements) | Prevents SQL injection — the #1 web vulnerability. Never concatenate user input into SQL strings. |
| ✓ | Rate limiting on submissions and checkout | Prevents brute-force attacks, form spam floods, and checkout abuse |
| ✓ | Webhook signature verification | Prevents fake webhook events that could create fraudulent orders or grant unauthorized access |
| ✓ | No card data on server (Stripe Checkout handles it) | Eliminates PCI scope. No card data = no card data breach = PCI SAQ-A (simplest compliance level) |
| ✓ | Input sanitization (htmlspecialchars) | Prevents XSS (cross-site scripting) — malicious JavaScript injected via form fields |
| ✓ | Email validation (filter_var) | Prevents malformed email addresses, header injection attacks, and bounce storms |
| ✓ | HTTPS everywhere | Encrypts all data in transit. Required by Stripe, expected by customers, rewarded by Google |
| ✓ | No secrets in source code | API keys, tokens, and passwords must live in .env files, never committed to git |
| ✓ | Error messages don't leak internals | Generic errors to users ("Something went wrong"), detailed errors to logs only. No stack traces, SQL errors, or file paths in responses. |
| ✓ | Honeypot on public forms | Catches automated bots without degrading UX for real users. No CAPTCHA needed. |
| ✓ | Admin endpoints require auth token | Every admin API route must verify the Bearer token before processing. No unprotected admin endpoints. |
Pre-Launch Verification
Before deploying to production, manually verify each item:
<script>alert('xss')</script> in every text field. Verify the output is escaped, not executed. Check the database too — stored XSS is worse than reflected.
grep -r "sk_live\|sk_test\|whsec_" . — there should be zero matches in source files.