Skip to content

Commit f70b66a

Browse files
v0.0.1
1 parent 57dcff5 commit f70b66a

19 files changed

Lines changed: 3219 additions & 1 deletion

.github/workflows/ci.yml

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
name: CI
2+
3+
on:
4+
push:
5+
branches: [main]
6+
pull_request:
7+
branches: [main]
8+
9+
jobs:
10+
build:
11+
name: Build & Test
12+
runs-on: ubuntu-latest
13+
steps:
14+
- uses: actions/checkout@v4
15+
- uses: actions/setup-node@v4
16+
with:
17+
node-version: 20
18+
cache: npm
19+
- run: npm ci
20+
- name: TypeScript check
21+
run: npm run typecheck
22+
- name: Tests
23+
run: npm test
24+
- name: Build
25+
run: npm run build
26+
- name: Security audit
27+
run: npm audit --audit-level=high

.gitignore

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
# Dependencies
2+
node_modules/
3+
4+
# Build output
5+
dist/
6+
.next/
7+
.astro/
8+
.vercel/
9+
.output/
10+
11+
# Cloudflare Workers local state
12+
.wrangler/
13+
.dev.vars
14+
15+
# Environment files (keep .env.example)
16+
.env
17+
.env.local
18+
.env.*.local
19+
20+
# OS files
21+
.DS_Store
22+
Thumbs.db
23+
24+
# Editor
25+
.vscode/
26+
.idea/
27+
*.swp
28+
*.swo
29+
30+
# TypeScript build info
31+
*.tsbuildinfo
32+
33+
# Logs
34+
*.log
35+
npm-debug.log*
36+
37+
# Claude Code project files
38+
CLAUDE.md
39+
.claude/
40+
tasks/

README.md

Lines changed: 139 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1,139 @@
1-
# cloudcore-ecom
1+
# Cloudcore Ecom
2+
3+
![CI](https://github.com/cloudcore-cms/cloudcore-ecom/actions/workflows/ci.yml/badge.svg)
4+
5+
E-commerce starter for [Cloudcore CMS](https://github.com/cloudcore-cms/cloudcore-cms). Adds products, subscriptions, Stripe, and PayPal to your CMS.
6+
7+
Not a standalone service — copy the source files into your CMS. One Worker, one database, one auth system.
8+
9+
## Setup
10+
11+
### 1. Copy ecom files into your CMS
12+
13+
```bash
14+
# Clone this repo
15+
git clone https://github.com/cloudcore-cms/cloudcore-ecom.git
16+
17+
# Copy the source files into your CMS
18+
cp -r cloudcore-ecom/src/* your-cms/src/ecom/
19+
20+
# Copy the migration
21+
cp cloudcore-ecom/migrations/001_ecom.sql your-cms/src/db/migrations/002_ecom.sql
22+
```
23+
24+
### 2. Run the migration
25+
26+
```bash
27+
npx wrangler d1 migrations apply cloudcore-db --file=src/db/migrations/002_ecom.sql
28+
```
29+
30+
### 3. Mount the routes in your CMS
31+
32+
```typescript
33+
// src/index.ts
34+
import { ecomRoutes, ecomPublicRoutes, ecomWebhookRoutes } from './ecom';
35+
36+
// Admin routes (behind auth)
37+
app.route('/api/v1/shop', ecomRoutes);
38+
39+
// Public product listing (no auth)
40+
app.route('/api/v1/public/shop', ecomPublicRoutes);
41+
42+
// Payment webhooks (verified by provider signatures, no auth)
43+
app.route('/api/v1/webhooks', ecomWebhookRoutes);
44+
```
45+
46+
### 4. Add environment variables
47+
48+
```toml
49+
# wrangler.toml [vars] or use wrangler secret put
50+
STRIPE_SECRET_KEY = ""
51+
STRIPE_WEBHOOK_SECRET = ""
52+
53+
# PayPal (optional)
54+
PAYPAL_CLIENT_ID = ""
55+
PAYPAL_CLIENT_SECRET = ""
56+
PAYPAL_WEBHOOK_ID = ""
57+
PAYPAL_MODE = "sandbox" # or "live"
58+
```
59+
60+
## API Endpoints
61+
62+
### Admin (requires CMS auth)
63+
64+
| Method | Endpoint | Description |
65+
|---|---|---|
66+
| `GET` | `/shop/products` | List products |
67+
| `GET` | `/shop/products/:id` | Get product |
68+
| `POST` | `/shop/products` | Create product |
69+
| `PATCH` | `/shop/products/:id` | Update product |
70+
| `DELETE` | `/shop/products/:id` | Delete product |
71+
| `POST` | `/shop/checkout` | Create checkout session |
72+
| `GET` | `/shop/subscriptions` | List all subscriptions |
73+
| `GET` | `/shop/subscriptions/my` | List current user's subscriptions |
74+
| `POST` | `/shop/subscriptions/:id/cancel` | Cancel subscription |
75+
76+
### Public (no auth)
77+
78+
| Method | Endpoint | Description |
79+
|---|---|---|
80+
| `GET` | `/public/shop/products` | List active products |
81+
| `GET` | `/public/shop/products/:slug` | Get product by slug |
82+
83+
### Webhooks (verified by provider)
84+
85+
| Method | Endpoint | Description |
86+
|---|---|---|
87+
| `POST` | `/webhooks/stripe` | Stripe webhook |
88+
| `POST` | `/webhooks/paypal` | PayPal webhook |
89+
90+
## Product Types
91+
92+
**One-time purchase:**
93+
```json
94+
{
95+
"name": "Pro Template",
96+
"type": "one_time",
97+
"price": 4900,
98+
"currency": "usd"
99+
}
100+
```
101+
102+
**Subscription:**
103+
```json
104+
{
105+
"name": "Pro Plan",
106+
"type": "subscription",
107+
"price": 1900,
108+
"currency": "usd",
109+
"intervalType": "month",
110+
"intervalCount": 1,
111+
"trialDays": 14
112+
}
113+
```
114+
115+
Prices are in cents (4900 = $49.00).
116+
117+
## Database Tables
118+
119+
All tables prefixed with `cc_ecom_` to avoid conflicts with CMS tables:
120+
121+
- `cc_ecom_products` — products and subscription plans
122+
- `cc_ecom_orders` — one-time purchase records
123+
- `cc_ecom_subscriptions` — active/canceled subscriptions
124+
- `cc_ecom_webhook_events` — idempotency log for payment webhooks
125+
126+
## How It Works
127+
128+
1. Admin creates products in the CMS (or via API)
129+
2. Frontend lists active products via the public endpoint
130+
3. User clicks "Buy" — frontend calls `/shop/checkout` with product ID and payment provider
131+
4. User is redirected to Stripe/PayPal hosted checkout
132+
5. On success, webhook creates order/subscription records in D1
133+
6. Frontend checks subscription status via `/shop/subscriptions/my`
134+
135+
No PCI compliance needed — all card handling is done by Stripe/PayPal.
136+
137+
## License
138+
139+
MIT

migrations/001_ecom.sql

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
-- Cloudcore Ecom Schema
2+
-- Run against the same D1 database as the CMS
3+
4+
-- Products
5+
CREATE TABLE IF NOT EXISTS cc_ecom_products (
6+
id TEXT PRIMARY KEY,
7+
name TEXT NOT NULL,
8+
slug TEXT NOT NULL UNIQUE,
9+
description TEXT,
10+
type TEXT NOT NULL DEFAULT 'one_time',
11+
status TEXT NOT NULL DEFAULT 'draft',
12+
price INTEGER NOT NULL,
13+
currency TEXT NOT NULL DEFAULT 'usd',
14+
compare_at_price INTEGER,
15+
interval_type TEXT,
16+
interval_count INTEGER,
17+
trial_days INTEGER,
18+
stripe_product_id TEXT,
19+
stripe_price_id TEXT,
20+
paypal_plan_id TEXT,
21+
image_url TEXT,
22+
metadata TEXT,
23+
sort_order INTEGER DEFAULT 0,
24+
created_at TEXT NOT NULL,
25+
updated_at TEXT
26+
);
27+
28+
CREATE INDEX IF NOT EXISTS idx_ecom_products_slug ON cc_ecom_products(slug);
29+
CREATE INDEX IF NOT EXISTS idx_ecom_products_status ON cc_ecom_products(status);
30+
CREATE INDEX IF NOT EXISTS idx_ecom_products_type ON cc_ecom_products(type);
31+
32+
-- Orders (one-time purchases)
33+
CREATE TABLE IF NOT EXISTS cc_ecom_orders (
34+
id TEXT PRIMARY KEY,
35+
user_id TEXT NOT NULL,
36+
product_id TEXT NOT NULL,
37+
status TEXT NOT NULL DEFAULT 'pending',
38+
amount INTEGER NOT NULL,
39+
currency TEXT NOT NULL DEFAULT 'usd',
40+
payment_provider TEXT,
41+
payment_id TEXT,
42+
payment_status TEXT,
43+
customer_email TEXT NOT NULL,
44+
customer_name TEXT,
45+
created_at TEXT NOT NULL,
46+
updated_at TEXT,
47+
FOREIGN KEY (user_id) REFERENCES cc_users(id),
48+
FOREIGN KEY (product_id) REFERENCES cc_ecom_products(id)
49+
);
50+
51+
CREATE INDEX IF NOT EXISTS idx_ecom_orders_user ON cc_ecom_orders(user_id);
52+
CREATE INDEX IF NOT EXISTS idx_ecom_orders_product ON cc_ecom_orders(product_id);
53+
CREATE INDEX IF NOT EXISTS idx_ecom_orders_status ON cc_ecom_orders(status);
54+
55+
-- Subscriptions
56+
CREATE TABLE IF NOT EXISTS cc_ecom_subscriptions (
57+
id TEXT PRIMARY KEY,
58+
user_id TEXT NOT NULL,
59+
product_id TEXT NOT NULL,
60+
status TEXT NOT NULL DEFAULT 'active',
61+
payment_provider TEXT NOT NULL,
62+
provider_subscription_id TEXT,
63+
provider_customer_id TEXT,
64+
current_period_start TEXT,
65+
current_period_end TEXT,
66+
cancel_at_period_end INTEGER DEFAULT 0,
67+
canceled_at TEXT,
68+
trial_end TEXT,
69+
created_at TEXT NOT NULL,
70+
updated_at TEXT,
71+
FOREIGN KEY (user_id) REFERENCES cc_users(id),
72+
FOREIGN KEY (product_id) REFERENCES cc_ecom_products(id)
73+
);
74+
75+
CREATE INDEX IF NOT EXISTS idx_ecom_subs_user ON cc_ecom_subscriptions(user_id);
76+
CREATE INDEX IF NOT EXISTS idx_ecom_subs_product ON cc_ecom_subscriptions(product_id);
77+
CREATE INDEX IF NOT EXISTS idx_ecom_subs_status ON cc_ecom_subscriptions(status);
78+
CREATE INDEX IF NOT EXISTS idx_ecom_subs_provider ON cc_ecom_subscriptions(payment_provider, provider_subscription_id);
79+
80+
-- Webhook Events (idempotency)
81+
CREATE TABLE IF NOT EXISTS cc_ecom_webhook_events (
82+
id TEXT PRIMARY KEY,
83+
provider TEXT NOT NULL,
84+
type TEXT NOT NULL,
85+
processed_at TEXT NOT NULL
86+
);
87+
88+
CREATE INDEX IF NOT EXISTS idx_ecom_webhooks_provider ON cc_ecom_webhook_events(provider, id);

0 commit comments

Comments
 (0)