Download OpenAPI specification:
Minimal photo printing API with Firebase Auth + R2 Direct Upload + Stripe Payment.
Implementation: Cloudflare Rate Limiting Rules (WAF-level)
Why Cloudflare Rules (NOT Workers KV):
Recommended Limits:
/api/auth/*: 10 req/min per IP/api/photos/*: 20 req/min per IP/api/stripe/checkout/*: 5 req/min per IP/api/stripe/webhook: No limit (Stripe IP whitelist only)/api/me/*: 30 req/min per IPResponse: HTTP 429 with Retry-After header
-- Users table (Firebase UID + Stripe Customer ID only)
CREATE TABLE users (
firebase_uid TEXT PRIMARY KEY,
stripe_customer_id TEXT UNIQUE,
brand_access TEXT, -- JSON: {"neko":true,"tokinoe":false,"dog":false}
created_at INTEGER NOT NULL,
updated_at INTEGER NOT NULL
);
-- Orders table (no PII, Stripe IDs only)
CREATE TABLE orders (
order_id TEXT PRIMARY KEY,
firebase_uid TEXT NOT NULL,
stripe_payment_intent_id TEXT UNIQUE NOT NULL,
stripe_checkout_session_id TEXT UNIQUE,
brand TEXT NOT NULL,
status TEXT DEFAULT 'pending',
total_amount INTEGER NOT NULL,
item_count INTEGER NOT NULL,
created_at INTEGER NOT NULL,
updated_at INTEGER NOT NULL,
FOREIGN KEY (firebase_uid) REFERENCES users(firebase_uid)
);
-- Order items (R2 keys only, no photo URLs)
CREATE TABLE order_items (
id TEXT PRIMARY KEY,
order_id TEXT NOT NULL,
r2_key TEXT NOT NULL, -- R2 path only, signed URLs generated on-demand
quantity INTEGER DEFAULT 1,
FOREIGN KEY (order_id) REFERENCES orders(order_id)
);
-- Webhook idempotency (event_id PRIMARY KEY = auto-dedup)
CREATE TABLE webhook_events (
event_id TEXT PRIMARY KEY, -- Stripe Event ID
event_type TEXT NOT NULL,
processed_at INTEGER NOT NULL
);
-- Performance indexes
CREATE INDEX idx_orders_user ON orders(firebase_uid, created_at DESC);
CREATE INDEX idx_webhook_events_processed ON webhook_events(processed_at);
Key Design Decisions:
webhook_events.event_id PRIMARY KEY → INSERT INTO ... VALUES (event.id, ...) auto-fails on duplicate = idempotencyr2_key stored (not full URL) → signed URLs generated on-demand with expirystripe_payment_intent_id UNIQUE → prevents duplicate ordersVerify Firebase ID token and create BFF session.
Security:
| idToken required | string Firebase ID token |
| brand required | string (Brand) Enum: "neko" "tokinoe" "dog" Supported brand names |
{- "idToken": "string",
- "brand": "neko"
}{- "user": {
- "firebase_uid": "string",
- "stripe_customer_id": "string",
- "brand_access": {
- "property1": true,
- "property2": true
}, - "active_brand": "neko",
- "created_at": "2019-08-24T14:15:22Z",
- "updated_at": "2019-08-24T14:15:22Z"
}, - "sessionToken": "string",
- "expiresAt": "2019-08-24T14:15:22Z"
}Retrieve products for a specific brand.
Cache Strategy:
| brand required | string (Brand) Enum: "neko" "tokinoe" "dog" Supported brand names |
| limit | integer [ 1 .. 100 ] Default: 20 |
| cursor | string |
{- "products": [
- {
- "id": "string",
- "title": "string",
- "handle": "string",
- "brand": "neko",
- "images": [
- {
- "id": "string",
- "url": "string",
- "alt_text": "string",
- "width": 0,
- "height": 0
}
], - "variants": [
- {
- "id": "string",
- "title": "string",
- "price": {
- "amount": "string",
- "currency_code": "JPY"
}, - "available": true,
- "inventory_quantity": 0
}
], - "price": {
- "amount": "string",
- "currency_code": "JPY"
}, - "available": true
}
], - "hasNext": true,
- "nextCursor": "string"
}| id required | string |
| brand required | string (Brand) Enum: "neko" "tokinoe" "dog" Supported brand names |
{- "product": {
- "id": "string",
- "title": "string",
- "handle": "string",
- "brand": "neko",
- "images": [
- {
- "id": "string",
- "url": "string",
- "alt_text": "string",
- "width": 0,
- "height": 0
}
], - "variants": [
- {
- "id": "string",
- "title": "string",
- "price": {
- "amount": "string",
- "currency_code": "JPY"
}, - "available": true,
- "inventory_quantity": 0
}
], - "price": {
- "amount": "string",
- "currency_code": "JPY"
}, - "available": true,
- "description": "string",
- "specifications": { }
}
}Returns R2 presigned PUT URL for client-side direct upload (no Workers streaming).
Flow:
Why Direct Upload:
Photo Requirements:
| brand required | string (Brand) Enum: "neko" "tokinoe" "dog" Supported brand names |
| filename required | string |
| content_type required | string Value: "image/jpeg" |
{- "brand": "neko",
- "filename": "cat_photo.jpg",
- "content_type": "image/jpeg"
}{- "photo_id": "photo_abc123xyz789",
- "r2_key": "photos/user123/abc123.jpg",
- "expires_at": 1696345200
}Verifies that a photo was successfully uploaded to R2 before allowing checkout.
Purpose:
Flow:
Security:
| photo_id required | string Photo ID returned from /api/photos/init |
| brand required | string (Brand) Enum: "neko" "tokinoe" "dog" Supported brand names |
{- "photo_id": "photo_abc123xyz789",
- "brand": "neko"
}{- "photo_id": "photo_abc123xyz789",
- "r2_key": "photos/user123/abc123.jpg",
- "verified_at": 1696345200,
- "file_size": 2048576
}Creates a Stripe Checkout Session for photo printing orders with built-in security features.
Security Features:
Metadata Stored (minimal, PII-free):
Payment Flow:
Idempotency:
| brand required | string (Brand) Enum: "neko" "tokinoe" "dog" Supported brand names |
required | Array of objects (StripeCartItem) [ 1 .. 100 ] items Cart items with photo references |
required | object (ShippingAddress) Japanese shipping address (PII - NEVER stored in D1). Storage Policy:
Retrieval:
|
{- "brand": "neko",
- "items": [
- {
- "product_id": "prod_neko_89mm_matte",
- "variant_id": "var_single_print",
- "quantity": 3,
- "price": 300,
- "photo_ids": [
- "photo_abc123xyz789",
- "photo_def456uvw012",
- "photo_ghi789rst345"
]
}
], - "shipping_address": {
- "postal_code": "150-0001",
- "prefecture": "東京都",
- "city": "渋谷区",
- "line1": "1-2-3",
- "line2": "サンプルマンション101",
- "name": "山田太郎",
- "phone": "03-1234-5678"
}
}{- "session_id": "cs_test_a1b2c3d4e5f6g7h8i9j0",
- "expires_at": 1696345200
}Receives and processes Stripe webhook events with cryptographic signature verification.
Accepted Events:
checkout.session.completed: Payment successful, create ordercheckout.session.expired: Session timeout, cleanup resourcespayment_intent.succeeded: Payment confirmedpayment_intent.payment_failed: Payment failed, notify usercharge.refunded: Refund processedIdempotency Guarantee via D1 UNIQUE constraint (no Queues/Durable Objects needed):
Flow:
INSERT OR IGNORE INTO webhook_events(id, ...) VALUES (event.id, ...)Why This Works:
event_id UNIQUE prevents duplicate processingNote: If traffic exceeds 10k orders/month, consider adding Queues for SKU-level serialization.
| stripe-signature required | string Example: t=1696345200,v1=abc123def456,v0=xyz789 Stripe webhook signature for verification |
| id | string Unique event identifier |
| type | string Event type |
| data | object Event payload data |
{- "id": "evt_1234567890abcdef",
- "type": "checkout.session.completed",
- "data": { }
}{- "received": true
}Creates a Stripe Customer Portal session URL for authenticated users to manage:
Prerequisites:
Session Flow:
| return_url | string <uri> Default: "https://neko.contents-print.jp/account" Example: return_url=https://neko.contents-print.jp/account/settings URL to redirect user after portal session ends |
| brand required | string (Brand) Enum: "neko" "tokinoe" "dog" Brand context for portal customization |
{- "expires_at": 1696345200
}Retrieve authenticated user's order history.
Data Source:
PII Policy:
| brand required | string (Brand) Enum: "neko" "tokinoe" "dog" Supported brand names |
| limit | integer [ 1 .. 100 ] Default: 20 |
| cursor | string |
{- "orders": [
- {
- "order_id": "string",
- "firebase_uid": "string",
- "stripe_customer_id": "string",
- "stripe_payment_intent_id": "string",
- "stripe_checkout_session_id": "string",
- "brand": "neko",
- "status": "pending",
- "total_amount": 0,
- "item_count": 0,
- "photo_urls": [
- "string"
], - "created_at": "2019-08-24T14:15:22Z",
- "updated_at": "2019-08-24T14:15:22Z"
}
], - "hasNext": true,
- "nextCursor": "string"
}