create-shopify-firebase-app 1.0.0 → 1.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # create-shopify-firebase-app
2
2
 
3
- > Create Shopify apps powered by Firebase. One command. Zero framework. Fully serverless.
3
+ > Build and run Shopify apps for free. Pay nothing until you have real traffic. One command. Zero framework. Fully serverless.
4
4
 
5
5
  [![npm version](https://img.shields.io/npm/v/create-shopify-firebase-app.svg)](https://www.npmjs.com/package/create-shopify-firebase-app)
6
6
  [![Downloads](https://img.shields.io/npm/dm/create-shopify-firebase-app.svg)](https://www.npmjs.com/package/create-shopify-firebase-app)
@@ -11,10 +11,9 @@ npx create-shopify-firebase-app my-app
11
11
  ```
12
12
 
13
13
  <p align="center">
14
- <img src="https://img.shields.io/badge/Shopify-App-7AB55C?logo=shopify&logoColor=white" />
15
- <img src="https://img.shields.io/badge/Firebase-Backend-FFCA28?logo=firebase&logoColor=black" />
14
+ <img src="https://img.shields.io/badge/Shopify-2026--01-7AB55C?logo=shopify&logoColor=white" />
15
+ <img src="https://img.shields.io/badge/Firebase-v2%20Functions-FFCA28?logo=firebase&logoColor=black" />
16
16
  <img src="https://img.shields.io/badge/TypeScript-Functions-3178C6?logo=typescript&logoColor=white" />
17
- <img src="https://img.shields.io/badge/Express-Server-000000?logo=express&logoColor=white" />
18
17
  </p>
19
18
 
20
19
  ---
@@ -23,12 +22,13 @@ npx create-shopify-firebase-app my-app
23
22
 
24
23
  The **Firebase alternative** to `shopify app init`. Instead of Remix + Prisma + Vercel, you get:
25
24
 
26
- - **Firebase Cloud Functions** (Express + TypeScript) for your backend
25
+ - **Firebase v2 Cloud Functions** (gen 2) 4 independent, auto-scaling TypeScript functions
27
26
  - **Cloud Firestore** for sessions and app data (auto-scaling, free tier)
28
27
  - **Firebase Hosting** for your embedded admin dashboard (free)
29
28
  - **Vanilla HTML/JS + App Bridge** for the frontend (no React, no build step)
30
29
  - **Theme App Extension** for storefront UI (works on all Shopify plans)
31
- - **Production-ready** OAuth, session tokens, webhooks, GDPR handlers — all included
30
+ - **Shopify API 2026-01** (latest) — OAuth, session tokens, webhooks, GDPR handlers
31
+ - **Production-ready** — deploy with one command, scale to millions
32
32
 
33
33
  One `npx` command scaffolds everything, installs dependencies, wires up Firebase, and initializes git. You're ready to `firebase deploy`.
34
34
 
@@ -117,31 +117,35 @@ shopify app dev
117
117
 
118
118
  ---
119
119
 
120
- ## Why Firebase instead of Remix?
120
+ ## Why Firebase?
121
+
122
+ **$0/month to run your Shopify app. No credit card. No server. No bill until you're big.**
123
+
124
+ Most Shopify app developers pay for hosting before they even have users. With Firebase, you deploy for free and only start paying when your app serves thousands of stores daily. Even at 50,000 installed stores, you're looking at ~$5/month. Try getting that from Vercel or Heroku.
121
125
 
122
126
  | | `shopify app init` (Remix) | `create-shopify-firebase-app` |
123
127
  |---|---|---|
124
- | **Backend** | Remix server | Firebase Cloud Functions (Express) |
128
+ | **Backend** | Remix server (monolith) | Firebase v2 Cloud Functions (4 independent functions) |
125
129
  | **Database** | Prisma + PostgreSQL | Cloud Firestore (NoSQL, auto-scaling) |
126
130
  | **Frontend** | React + Polaris | Vanilla HTML/JS + App Bridge |
127
131
  | **Hosting** | Vercel / Fly.io / Heroku | Firebase Hosting (free tier) |
128
132
  | **Auth** | `@shopify/shopify-app-remix` | Manual OAuth (140 lines, you own it) |
129
133
  | **Build** | Webpack / Vite | `tsc` (TypeScript compiler, no bundler) |
130
134
  | **Deploy** | Varies | `firebase deploy` (one command) |
131
- | **Cost** | $5-25/month hosting | Free tier covers most apps |
135
+ | **Cost** | $5-25/month from day one | **$0/month** free until you scale |
132
136
  | **Framework knowledge** | Remix + React required | Express + HTML (that's it) |
133
- | **Backend code** | ~2000+ lines (framework) | ~350 lines (you own all of it) |
137
+ | **Scaling** | Single server | Per-function auto-scaling (Cloud Run) |
134
138
  | **GDPR webhooks** | Auto-handled | Included (ready for App Store) |
135
139
  | **Theme extensions** | Supported | Supported (same Shopify format) |
136
140
  | **Shopify Functions** | Supported | Supported (add via Shopify CLI) |
137
141
 
138
142
  ### When to use this
139
143
 
144
+ - You want to **launch for free** and only pay when your app takes off
140
145
  - Custom apps for a single merchant
141
146
  - Public apps with simple admin UIs
142
147
  - Teams already using Firebase / Google Cloud
143
148
  - You want to understand every line of your app
144
- - You want free/cheap serverless hosting
145
149
 
146
150
  ### When to use Remix instead
147
151
 
@@ -155,18 +159,18 @@ shopify app dev
155
159
 
156
160
  ```
157
161
  my-app/
158
- ├── shopify.app.toml # Shopify app config
162
+ ├── shopify.app.toml # Shopify app config (API 2026-01)
159
163
  ├── firebase.json # Firebase Hosting + Functions + Firestore
160
164
  ├── firestore.rules # Security rules (blocks direct client access)
161
165
 
162
- ├── functions/ # ── Backend ──
166
+ ├── functions/ # ── Backend (4 Cloud Functions) ──
163
167
  │ ├── src/
164
- │ │ ├── index.ts # Express app + Cloud Function export
165
- │ │ ├── auth.ts # OAuth 2.0 (install + callback + token storage)
168
+ │ │ ├── index.ts # Function exports (auth, api, webhooks, proxy)
169
+ │ │ ├── auth.ts # OAuth 2.0 (standalone no Express)
166
170
  │ │ ├── verify-token.ts # App Bridge JWT session token middleware
167
- │ │ ├── admin-api.ts # Your admin dashboard API routes
168
- │ │ ├── proxy.ts # Storefront-facing App Proxy routes
169
- │ │ ├── webhooks.ts # Webhook handlers (uninstall + GDPR)
171
+ │ │ ├── admin-api.ts # Admin dashboard API routes (Express)
172
+ │ │ ├── proxy.ts # Storefront App Proxy routes (Express)
173
+ │ │ ├── webhooks.ts # Webhook handlers (standalone no Express)
170
174
  │ │ ├── firebase.ts # Firebase Admin SDK init
171
175
  │ │ └── config.ts # Environment config
172
176
  │ └── .env # Your secrets (auto-generated, git-ignored)
@@ -187,6 +191,8 @@ my-app/
187
191
 
188
192
  ## Architecture
189
193
 
194
+ Each function scales independently on Cloud Run (Firebase v2 / gen 2):
195
+
190
196
  ```
191
197
  Shopify Admin (iframe)
192
198
  ┌─────────────────────────────────────┐
@@ -195,12 +201,12 @@ my-app/
195
201
  └──────────────┬──────────────────────┘
196
202
  │ Bearer <JWT>
197
203
 
198
- Firebase Cloud Functions (Express)
204
+ Firebase v2 Cloud Functions (independent scaling)
199
205
  ┌─────────────────────────────────────┐
200
- /auth → OAuth 2.0 flow
201
- /api/* → Admin API (JWT auth)
202
- /proxy/* App Proxy (HMAC) │
203
- /webhooks Webhooks (HMAC)
206
+ │ auth() → OAuth 2.0 flow (standalone, no Express)
207
+ │ api() → Admin API (JWT) │ (Express + middleware)
208
+ webhooks() Webhooks (HMAC) │ (standalone, no Express)
209
+ proxy() App Proxy (HMAC) (Express)
204
210
  └──────────────┬──────────────────────┘
205
211
 
206
212
 
@@ -212,6 +218,16 @@ my-app/
212
218
  └─────────────────────────────────────┘
213
219
  ```
214
220
 
221
+ ### Why split functions?
222
+
223
+ | Benefit | How |
224
+ |---------|-----|
225
+ | **Faster webhooks** | `webhooks()` has no Express overhead — responds in milliseconds |
226
+ | **Independent scaling** | Each function auto-scales based on its own traffic |
227
+ | **Targeted deploys** | `firebase deploy --only functions:api` deploys just one function |
228
+ | **Separate configs** | Each function gets its own memory, timeout, and concurrency |
229
+ | **Lower cold starts** | Smaller functions = faster cold starts |
230
+
215
231
  ### Three Security Layers
216
232
 
217
233
  | Layer | Protects | How |
@@ -265,15 +281,56 @@ shopify app dev
265
281
  ### Deploy
266
282
 
267
283
  ```bash
268
- firebase deploy # Everything
269
- firebase deploy --only functions # Backend only
270
- firebase deploy --only hosting # Frontend only
284
+ firebase deploy # Everything
285
+ firebase deploy --only functions # All functions
286
+ firebase deploy --only functions:auth # Just auth function
287
+ firebase deploy --only functions:api # Just API function
288
+ firebase deploy --only hosting # Frontend only
271
289
  ```
272
290
 
273
291
  ---
274
292
 
275
293
  ## Extending Your App
276
294
 
295
+ ### Add a new Cloud Function
296
+
297
+ This is the most common extension pattern. Create a new file, export a handler, and wire it up:
298
+
299
+ **1. Create `functions/src/my-feature.ts`:**
300
+
301
+ ```typescript
302
+ import type { Request, Response } from "firebase-functions/v2/https";
303
+ import { db } from "./firebase";
304
+
305
+ export async function myFeatureHandler(req: Request, res: Response) {
306
+ // Your logic here
307
+ res.json({ success: true });
308
+ }
309
+ ```
310
+
311
+ **2. Export it in `functions/src/index.ts`:**
312
+
313
+ ```typescript
314
+ import { myFeatureHandler } from "./my-feature";
315
+
316
+ export const myFeature = onRequest(
317
+ { memory: "256MiB", timeoutSeconds: 60 },
318
+ myFeatureHandler,
319
+ );
320
+ ```
321
+
322
+ **3. Add a rewrite in `firebase.json`:**
323
+
324
+ ```json
325
+ { "source": "/my-feature/**", "function": "myFeature" }
326
+ ```
327
+
328
+ **4. Deploy just your function:**
329
+
330
+ ```bash
331
+ firebase deploy --only functions:myFeature
332
+ ```
333
+
277
334
  ### Add admin API routes
278
335
 
279
336
  Edit `functions/src/admin-api.ts`:
@@ -304,7 +361,7 @@ Edit `functions/src/webhooks.ts` and register in `shopify.app.toml`:
304
361
 
305
362
  ```typescript
306
363
  case "orders/create": {
307
- const order = req.body;
364
+ const order = body;
308
365
  // Your logic
309
366
  break;
310
367
  }
@@ -325,21 +382,68 @@ Create `web/settings.html`, use the same pattern, navigate with `App.navigate("/
325
382
 
326
383
  Use the `appSubscriptionCreate` GraphQL mutation in your admin API routes.
327
384
 
385
+ ### Add a scheduled function (cron)
386
+
387
+ ```typescript
388
+ // functions/src/cleanup.ts
389
+ import { onSchedule } from "firebase-functions/v2/scheduler";
390
+ import { db } from "./firebase";
391
+
392
+ export const dailyCleanup = onSchedule("every 24 hours", async () => {
393
+ // Clean up expired nonces, old data, etc.
394
+ const cutoff = new Date(Date.now() - 48 * 60 * 60 * 1000);
395
+ const old = await db.collection("authNonces")
396
+ .where("createdAt", "<", cutoff.toISOString()).get();
397
+ for (const doc of old.docs) await doc.ref.delete();
398
+ });
399
+ ```
400
+
401
+ Export it in `index.ts`:
402
+ ```typescript
403
+ export { dailyCleanup } from "./cleanup";
404
+ ```
405
+
328
406
  ---
329
407
 
330
- ## Firebase Free Tier
408
+ ## How Many Stores Can You Run for Free?
409
+
410
+ Firebase's free tier is generous. Here's what it actually means for a Shopify app:
411
+
412
+ ### Per-store usage (typical Shopify app)
413
+
414
+ | Action | Function calls | Firestore reads | Firestore writes |
415
+ |--------|---------------|-----------------|------------------|
416
+ | Merchant opens admin dashboard | 5 | 5 | 0 |
417
+ | Webhooks (orders, inventory) | 5 | 5 | 2 |
418
+ | **Total per active store/day** | **~10** | **~10** | **~2** |
419
+
420
+ ### Free tier capacity
421
+
422
+ | Firebase Resource | Free Limit | Stores Supported |
423
+ |-------------------|-----------|-----------------|
424
+ | Cloud Functions | 2M invocations/month (~66K/day) | **~6,600 daily active stores** |
425
+ | Firestore reads | 50K/day | **~5,000 daily active stores** |
426
+ | Firestore writes | 20K/day | **~10,000 daily active stores** |
427
+ | Hosting bandwidth | 360 MB/day | **~7,000 page loads/day** (CDN-cached after first load) |
428
+
429
+ **Bottleneck: Firestore reads at ~5,000 daily active stores.**
430
+
431
+ Not every installed merchant opens your app daily. With a typical 20% daily active rate:
432
+
433
+ > **Free tier supports ~25,000 installed stores** with normal usage patterns. That's $0/month.
434
+
435
+ ### When you outgrow free (Blaze pay-as-you-go)
331
436
 
332
- The Spark (free) plan covers most Shopify apps:
437
+ | Installed Stores | Daily Active | Monthly Cost |
438
+ |-----------------|-------------|-------------|
439
+ | 1 - 25,000 | up to 5,000 | **$0 (free)** |
440
+ | 50,000 | ~10,000 | **~$5/month** |
441
+ | 100,000 | ~20,000 | **~$15/month** |
442
+ | 500,000 | ~100,000 | **~$80/month** |
333
443
 
334
- | Resource | Free Limit |
335
- |----------|-----------|
336
- | Cloud Functions | 2M invocations/month |
337
- | Firestore reads | 50K/day |
338
- | Firestore writes | 20K/day |
339
- | Hosting storage | 10 GB |
340
- | Hosting transfer | 360 MB/day |
444
+ Compare that to Vercel/Heroku at **$25-100/month from day one**, before you even have your first user.
341
445
 
342
- Need more? The Blaze plan (pay-as-you-go) costs most apps **under $5/month**.
446
+ No credit card required to start. No server to manage. No bill until you're successful.
343
447
 
344
448
  ---
345
449
 
@@ -364,8 +468,9 @@ These are **required** for Shopify App Store listing.
364
468
  | "Missing shop parameter" | Set **App URL** in Partner Dashboard to `https://PROJECT_ID.web.app` |
365
469
  | "HMAC verification failed" | Check `SHOPIFY_API_SECRET` in `functions/.env` |
366
470
  | "Invalid session token" | Verify `data-api-key` in `web/index.html` matches your API key |
367
- | Functions not receiving requests | Check `firebase.json` rewrites and run `firebase functions:list` |
471
+ | Functions not receiving requests | Check `firebase.json` rewrites match function export names in `index.ts` |
368
472
  | Webhook failures | Must respond 200 within 5 seconds. Check `firebase functions:log` |
473
+ | Individual function not deploying | Ensure export name in `index.ts` matches function name in `firebase.json` rewrite |
369
474
 
370
475
  ---
371
476
 
@@ -385,7 +490,7 @@ npm link # Test locally: create-shopify-firebase-app test-app
385
490
  ## Related
386
491
 
387
492
  - [Shopify App Development](https://shopify.dev/docs/apps) — Official docs
388
- - [Firebase Cloud Functions](https://firebase.google.com/docs/functions) — Backend runtime
493
+ - [Firebase v2 Cloud Functions](https://firebase.google.com/docs/functions) — Backend runtime (gen 2)
389
494
  - [Shopify App Bridge](https://shopify.dev/docs/api/app-bridge) — Embedded app SDK
390
495
  - [Shopify Admin GraphQL API](https://shopify.dev/docs/api/admin-graphql) — Store data API
391
496
  - [Theme App Extensions](https://shopify.dev/docs/apps/build/online-store/theme-app-extensions) — Storefront blocks
package/lib/index.js CHANGED
@@ -478,7 +478,8 @@ function printHelp() {
478
478
 
479
479
  ${c.bold}What you get:${c.reset}
480
480
 
481
- Express + TypeScript Cloud Functions (OAuth, webhooks, GDPR)
481
+ Firebase v2 Cloud Functions (4 independent, auto-scaling functions)
482
+ ✔ Shopify API 2026-01 (latest) + OAuth, webhooks, GDPR
482
483
  ✔ Firestore for sessions and app data
483
484
  ✔ App Bridge embedded admin dashboard (vanilla HTML/JS)
484
485
  ✔ Theme App Extension for storefront UI
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-shopify-firebase-app",
3
- "version": "1.0.0",
3
+ "version": "1.1.1",
4
4
  "description": "Create Shopify apps powered by Firebase — serverless, lightweight, zero-framework. The official alternative to Remix for Shopify + Firebase developers.",
5
5
  "keywords": [
6
6
  "shopify",
@@ -3,20 +3,23 @@
3
3
  "public": "web",
4
4
  "ignore": ["firebase.json", "**/.*", "**/node_modules/**"],
5
5
  "rewrites": [
6
- { "source": "/auth", "function": "app" },
7
- { "source": "/auth/**", "function": "app" },
8
- { "source": "/api", "function": "app" },
9
- { "source": "/api/**", "function": "app" },
10
- { "source": "/proxy", "function": "app" },
11
- { "source": "/proxy/**", "function": "app" },
12
- { "source": "/webhooks", "function": "app" },
13
- { "source": "/webhooks/**", "function": "app" }
6
+ { "source": "/auth", "run": { "serviceId": "auth", "region": "us-central1" } },
7
+ { "source": "/auth/**", "run": { "serviceId": "auth", "region": "us-central1" } },
8
+ { "source": "/api", "run": { "serviceId": "api", "region": "us-central1" } },
9
+ { "source": "/api/**", "run": { "serviceId": "api", "region": "us-central1" } },
10
+ { "source": "/proxy", "run": { "serviceId": "proxy", "region": "us-central1" } },
11
+ { "source": "/proxy/**", "run": { "serviceId": "proxy", "region": "us-central1" } },
12
+ { "source": "/webhooks", "run": { "serviceId": "webhooks", "region": "us-central1" } },
13
+ { "source": "/webhooks/**", "run": { "serviceId": "webhooks", "region": "us-central1" } }
14
14
  ]
15
15
  },
16
- "functions": {
17
- "source": "functions",
18
- "runtime": "nodejs20"
19
- },
16
+ "functions": [
17
+ {
18
+ "source": "functions",
19
+ "codebase": "default",
20
+ "runtime": "nodejs20"
21
+ }
22
+ ],
20
23
  "firestore": {
21
24
  "rules": "firestore.rules",
22
25
  "indexes": "firestore.indexes.json"
@@ -9,21 +9,23 @@
9
9
  "build": "tsc",
10
10
  "serve": "npm run build && firebase emulators:start --only functions,firestore",
11
11
  "deploy": "firebase deploy --only functions",
12
+ "deploy:auth": "firebase deploy --only functions:auth",
13
+ "deploy:api": "firebase deploy --only functions:api",
14
+ "deploy:webhooks": "firebase deploy --only functions:webhooks",
15
+ "deploy:proxy": "firebase deploy --only functions:proxy",
12
16
  "deploy:all": "firebase deploy"
13
17
  },
14
18
  "dependencies": {
15
19
  "cors": "^2.8.5",
16
20
  "express": "^4.21.0",
17
- "firebase-admin": "^12.7.0",
18
- "firebase-functions": "^5.1.1",
19
- "jsonwebtoken": "^9.0.2",
20
- "node-fetch": "^2.7.0"
21
+ "firebase-admin": "^13.0.0",
22
+ "firebase-functions": "^6.3.0",
23
+ "jsonwebtoken": "^9.0.2"
21
24
  },
22
25
  "devDependencies": {
23
26
  "@types/cors": "^2.8.17",
24
- "@types/express": "^5.0.0",
27
+ "@types/express": "^4.17.21",
25
28
  "@types/jsonwebtoken": "^9.0.7",
26
- "@types/node-fetch": "^2.6.12",
27
29
  "typescript": "^5.6.3"
28
30
  }
29
31
  }
@@ -1,5 +1,4 @@
1
1
  import { Router, Request, Response } from "express";
2
- import fetch from "node-fetch";
3
2
  import { verifySessionToken } from "./verify-token";
4
3
  import { getAccessToken } from "./auth";
5
4
 
@@ -8,6 +7,10 @@ export const adminApiRouter = Router();
8
7
  // All admin routes require session token verification
9
8
  adminApiRouter.use(verifySessionToken);
10
9
 
10
+ // Shopify API version — update when Shopify releases new versions
11
+ // Docs: https://shopify.dev/docs/api/usage/versioning
12
+ const API_VERSION = "2026-01";
13
+
11
14
  // ─── Get shop info ───────────────────────────────────────────────────────
12
15
  adminApiRouter.get("/shop", async (req: Request, res: Response) => {
13
16
  const shop = (req as any).shopDomain;
@@ -20,7 +23,7 @@ adminApiRouter.get("/shop", async (req: Request, res: Response) => {
20
23
 
21
24
  try {
22
25
  const response = await fetch(
23
- `https://${shop}/admin/api/2025-04/graphql.json`,
26
+ `https://${shop}/admin/api/${API_VERSION}/graphql.json`,
24
27
  {
25
28
  method: "POST",
26
29
  headers: {
@@ -54,7 +57,7 @@ adminApiRouter.get("/products/search", async (req: Request, res: Response) => {
54
57
 
55
58
  try {
56
59
  const response = await fetch(
57
- `https://${shop}/admin/api/2025-04/graphql.json`,
60
+ `https://${shop}/admin/api/${API_VERSION}/graphql.json`,
58
61
  {
59
62
  method: "POST",
60
63
  headers: {
@@ -108,9 +111,15 @@ adminApiRouter.get("/products/search", async (req: Request, res: Response) => {
108
111
  });
109
112
 
110
113
  // ──────────────────────────────────────────────────────────────────────────
111
- // Add your admin API routes below.
112
- // All routes are protected by session token verification.
114
+ // HOW TO ADD A NEW ADMIN API ROUTE:
115
+ //
116
+ // adminApiRouter.post("/my-endpoint", async (req, res) => {
117
+ // const shop = (req as any).shopDomain;
118
+ // const accessToken = await getAccessToken(shop);
119
+ // // Call Shopify Admin API, write to Firestore, etc.
120
+ // res.json({ success: true });
121
+ // });
113
122
  //
114
- // const shop = (req as any).shopDomain;
115
- // const accessToken = await getAccessToken(shop);
123
+ // All routes are automatically protected by JWT session token verification.
124
+ // Deploy: firebase deploy --only functions:api
116
125
  // ──────────────────────────────────────────────────────────────────────────
@@ -1,14 +1,33 @@
1
- import { Router, Request, Response } from "express";
2
1
  import crypto from "crypto";
3
- import fetch from "node-fetch";
4
2
  import { getConfig } from "./config";
5
3
  import { db } from "./firebase";
4
+ import type { Request } from "firebase-functions/v2/https";
5
+
6
+ /**
7
+ * Standalone OAuth handler — no Express, no middleware overhead.
8
+ *
9
+ * Routes:
10
+ * GET /auth → Start OAuth (redirect to Shopify consent screen)
11
+ * GET /auth/callback → Handle callback (exchange code, store session)
12
+ */
13
+ export async function authHandler(req: Request, res: any): Promise<void> {
14
+ const urlPath = req.path;
15
+
16
+ if (req.method !== "GET") {
17
+ res.status(405).send("Method not allowed");
18
+ return;
19
+ }
6
20
 
7
- export const authRouter = Router();
21
+ if (urlPath === "/auth/callback") {
22
+ await handleCallback(req, res);
23
+ } else {
24
+ handleStart(req, res);
25
+ }
26
+ }
8
27
 
9
28
  // ─── Step 1: Start OAuth ─────────────────────────────────────────────────
10
29
  // Merchant clicks "Install" → redirect to Shopify consent screen.
11
- authRouter.get("/", (req: Request, res: Response) => {
30
+ function handleStart(req: Request, res: any): void {
12
31
  const { shop } = req.query;
13
32
  if (!shop || typeof shop !== "string") {
14
33
  res.status(400).send("Missing shop parameter");
@@ -33,11 +52,11 @@ authRouter.get("/", (req: Request, res: Response) => {
33
52
  `&state=${nonce}`;
34
53
 
35
54
  res.redirect(authUrl);
36
- });
55
+ }
37
56
 
38
57
  // ─── Step 2: OAuth Callback ──────────────────────────────────────────────
39
58
  // Shopify redirects back with code + HMAC. Verify, exchange, store session.
40
- authRouter.get("/callback", async (req: Request, res: Response) => {
59
+ async function handleCallback(req: Request, res: any): Promise<void> {
41
60
  const { shop, code, hmac, state } = req.query;
42
61
 
43
62
  if (!shop || !code || !hmac) {
@@ -47,7 +66,7 @@ authRouter.get("/callback", async (req: Request, res: Response) => {
47
66
 
48
67
  const config = getConfig();
49
68
 
50
- // Verify HMAC (primary security check — timing-safe)
69
+ // Verify HMAC (timing-safe comparison)
51
70
  const queryParams = { ...req.query };
52
71
  delete queryParams.hmac;
53
72
  delete queryParams.signature;
@@ -120,7 +139,7 @@ authRouter.get("/callback", async (req: Request, res: Response) => {
120
139
  console.error("OAuth error:", err);
121
140
  res.status(500).send("OAuth error");
122
141
  }
123
- });
142
+ }
124
143
 
125
144
  // Helper: get stored access token for a shop
126
145
  export async function getAccessToken(shop: string): Promise<string | null> {
@@ -1,40 +1,93 @@
1
- import * as functions from "firebase-functions";
2
- import "./firebase"; // Initialize Firebase first must be before other imports
1
+ /**
2
+ * Cloud Function exportseach function scales independently.
3
+ *
4
+ * Firebase v2 (gen 2) functions run on Cloud Run with per-function
5
+ * concurrency, memory, timeout, and min-instance settings.
6
+ *
7
+ * Architecture:
8
+ * auth — OAuth 2.0 install + callback (standalone, no Express)
9
+ * api — Admin dashboard API routes (Express + JWT middleware)
10
+ * webhooks — Webhook handlers (standalone, no Express — fast cold starts)
11
+ * proxy — Storefront App Proxy routes (Express + HMAC verification)
12
+ *
13
+ * Adding a new function:
14
+ * 1. Create a new file in src/ (e.g. src/cron.ts)
15
+ * 2. Export a handler from it
16
+ * 3. Import and re-export here with desired options
17
+ * 4. Add a rewrite in firebase.json if it needs an HTTP endpoint
18
+ * 5. Run: firebase deploy --only functions:yourFunction
19
+ *
20
+ * Docs: https://firebase.google.com/docs/functions/http-events?gen=2nd
21
+ */
22
+
23
+ import "./firebase"; // Initialize Firebase Admin SDK — must be first
24
+
25
+ import { onRequest } from "firebase-functions/v2/https";
3
26
  import express from "express";
4
27
  import cors from "cors";
5
- import { authRouter } from "./auth";
28
+ import { authHandler } from "./auth";
6
29
  import { adminApiRouter } from "./admin-api";
7
30
  import { proxyRouter } from "./proxy";
8
- import { webhookRouter } from "./webhooks";
9
-
10
- const expressApp = express();
11
- expressApp.use(cors({ origin: true }));
12
-
13
- // Capture raw body for webhook HMAC verification.
14
- // Shopify signs the raw request body — we need it before JSON parsing.
15
- expressApp.use(
16
- express.json({
17
- limit: "2mb",
18
- verify: (req: any, _res, buf) => {
19
- req.rawBody = buf.toString("utf8");
20
- },
21
- }),
31
+ import { webhookHandler } from "./webhooks";
32
+
33
+ // ─── auth: OAuth 2.0 install + callback ──────────────────────────────────
34
+ // Standalone handler (no Express overhead). Handles:
35
+ // GET /auth → redirect to Shopify consent screen
36
+ // GET /auth/callback exchange code for access token
37
+ export const auth = onRequest(
38
+ { memory: "256MiB", timeoutSeconds: 30, invoker: "public" },
39
+ authHandler,
40
+ );
41
+
42
+ // ─── api: Admin dashboard API ────────────────────────────────────────────
43
+ // Express app with JWT session token middleware on all routes.
44
+ // Add routes in src/admin-api.ts.
45
+ const apiApp = express();
46
+ apiApp.use(cors({ origin: true }));
47
+ apiApp.use(express.json());
48
+ apiApp.use("/api", adminApiRouter);
49
+
50
+ export const api = onRequest(
51
+ { memory: "256MiB", timeoutSeconds: 60, invoker: "public" },
52
+ apiApp,
22
53
  );
23
- expressApp.use(express.urlencoded({ extended: true }));
24
-
25
- // ─── Routes ────────────────────────────────────────────────────────────────
26
- expressApp.use("/auth", authRouter); // OAuth install + callback
27
- expressApp.use("/api", adminApiRouter); // Admin dashboard API (JWT auth)
28
- expressApp.use("/proxy", proxyRouter); // App Proxy routes (HMAC auth)
29
- expressApp.use("/webhooks", webhookRouter); // Webhook handlers
30
-
31
- // Health check
32
- expressApp.get("/", (_req, res) => {
33
- res.json({ status: "ok", timestamp: new Date().toISOString() });
34
- });
35
-
36
- // Export as a single Cloud Function.
37
- // Firebase Hosting rewrites (firebase.json) forward requests here.
38
- export const app = functions
39
- .runWith({ timeoutSeconds: 60, memory: "256MB" })
40
- .https.onRequest(expressApp);
54
+
55
+ // ─── webhooks: Shopify webhook handlers ──────────────────────────────────
56
+ // Standalone handler for maximum speed. Must respond 200 within 5 seconds.
57
+ // No Express, no CORS, no JSON parsing — just raw body HMAC verification.
58
+ export const webhooks = onRequest(
59
+ { memory: "256MiB", timeoutSeconds: 10, invoker: "public" },
60
+ webhookHandler,
61
+ );
62
+
63
+ // ─── proxy: Storefront App Proxy routes ──────────────────────────────────
64
+ // Express app for storefront-facing endpoints.
65
+ // Add routes in src/proxy.ts. Enable App Proxy in shopify.app.toml.
66
+ const proxyApp = express();
67
+ proxyApp.use(cors({ origin: true }));
68
+ proxyApp.use(express.json());
69
+ proxyApp.use("/proxy", proxyRouter);
70
+
71
+ export const proxy = onRequest(
72
+ { memory: "256MiB", timeoutSeconds: 30, invoker: "public" },
73
+ proxyApp,
74
+ );
75
+
76
+ // ──────────────────────────────────────────────────────────────────────────
77
+ // HOW TO ADD A NEW FUNCTION:
78
+ //
79
+ // // 1. Create src/my-feature.ts with your handler
80
+ // import { myFeatureHandler } from "./my-feature";
81
+ //
82
+ // // 2. Export it here with desired options
83
+ // export const myFeature = onRequest(
84
+ // { memory: "256MiB", timeoutSeconds: 60, invoker: "public" },
85
+ // myFeatureHandler,
86
+ // );
87
+ //
88
+ // // 3. Add rewrite in firebase.json:
89
+ // // { "source": "/my-feature/**", "run": { "serviceId": "myFeature", "region": "us-central1" } }
90
+ //
91
+ // // 4. Deploy only your function:
92
+ // // firebase deploy --only functions:myFeature
93
+ // ──────────────────────────────────────────────────────────────────────────
@@ -44,7 +44,15 @@ proxyRouter.get("/hello", (req: Request, res: Response) => {
44
44
  });
45
45
 
46
46
  // ──────────────────────────────────────────────────────────────────────────
47
- // Add storefront-facing routes below.
48
- // Always verify the proxy signature first.
49
- // Enable App Proxy in shopify.app.toml.
47
+ // HOW TO ADD A NEW PROXY ROUTE:
48
+ //
49
+ // proxyRouter.get("/my-route", (req, res) => {
50
+ // if (!verifyProxySignature(req.query as Record<string, any>)) {
51
+ // return res.status(403).json({ error: "Invalid signature" });
52
+ // }
53
+ // res.json({ hello: "storefront" });
54
+ // });
55
+ //
56
+ // Enable App Proxy in shopify.app.toml (uncomment the [app_proxy] section).
57
+ // Deploy: firebase deploy --only functions:proxy
50
58
  // ──────────────────────────────────────────────────────────────────────────
@@ -1,16 +1,14 @@
1
- import { Router, Request, Response } from "express";
2
1
  import crypto from "crypto";
3
2
  import { getConfig } from "./config";
4
3
  import { db } from "./firebase";
5
-
6
- export const webhookRouter = Router();
4
+ import type { Request } from "firebase-functions/v2/https";
7
5
 
8
6
  // ─── Verify Shopify Webhook HMAC ─────────────────────────────────────────
9
- function verifyWebhookHmac(rawBody: string, hmacHeader: string): boolean {
7
+ function verifyWebhookHmac(rawBody: Buffer, hmacHeader: string): boolean {
10
8
  const config = getConfig();
11
9
  const hash = crypto
12
10
  .createHmac("sha256", config.apiSecret)
13
- .update(rawBody, "utf8")
11
+ .update(rawBody)
14
12
  .digest("base64");
15
13
 
16
14
  try {
@@ -20,18 +18,26 @@ function verifyWebhookHmac(rawBody: string, hmacHeader: string): boolean {
20
18
  }
21
19
  }
22
20
 
23
- // ─── Webhook Handler ─────────────────────────────────────────────────────
24
- // All topics route to this single POST endpoint.
25
- // Topic is identified via X-Shopify-Topic header.
26
- // IMPORTANT: Respond 200 within 5 seconds. Do heavy work async.
27
- webhookRouter.post("/", async (req: Request, res: Response) => {
21
+ /**
22
+ * Standalone webhook handler no Express, no middleware.
23
+ *
24
+ * Why standalone? Webhooks must respond 200 within 5 seconds.
25
+ * Skipping Express middleware means faster cold starts and less overhead.
26
+ * Uses req.rawBody (provided natively by Firebase v2) for HMAC verification.
27
+ */
28
+ export async function webhookHandler(req: Request, res: any): Promise<void> {
29
+ // Only accept POST
30
+ if (req.method !== "POST") {
31
+ res.status(405).send("Method not allowed");
32
+ return;
33
+ }
34
+
28
35
  const hmac = req.headers["x-shopify-hmac-sha256"] as string;
29
36
  const topic = req.headers["x-shopify-topic"] as string;
30
37
  const shop = req.headers["x-shopify-shop-domain"] as string;
31
38
 
32
- // Verify HMAC
33
- const rawBody = (req as any).rawBody;
34
- if (rawBody && hmac && !verifyWebhookHmac(rawBody, hmac)) {
39
+ // Verify HMAC using rawBody (Buffer, provided by Firebase v2)
40
+ if (req.rawBody && hmac && !verifyWebhookHmac(req.rawBody, hmac)) {
35
41
  console.error("Webhook HMAC verification failed");
36
42
  res.status(401).send("Unauthorized");
37
43
  return;
@@ -39,6 +45,14 @@ webhookRouter.post("/", async (req: Request, res: Response) => {
39
45
 
40
46
  console.log(`Webhook: ${topic} from ${shop}`);
41
47
 
48
+ // Parse body
49
+ let body: any = {};
50
+ try {
51
+ body = JSON.parse(req.rawBody?.toString("utf8") || "{}");
52
+ } catch {
53
+ // Non-JSON webhook payloads are rare but valid
54
+ }
55
+
42
56
  switch (topic) {
43
57
  // ── App lifecycle ──────────────────────────────────────────────────
44
58
  case "app/uninstalled": {
@@ -73,5 +87,24 @@ webhookRouter.post("/", async (req: Request, res: Response) => {
73
87
  console.log(`Unhandled webhook: ${topic}`);
74
88
  }
75
89
 
90
+ // Always respond 200 quickly — do heavy work asynchronously
76
91
  res.status(200).send("OK");
77
- });
92
+ }
93
+
94
+ // ──────────────────────────────────────────────────────────────────────────
95
+ // HOW TO ADD A NEW WEBHOOK HANDLER:
96
+ //
97
+ // 1. Register the topic in shopify.app.toml:
98
+ // [[webhooks.subscriptions]]
99
+ // topics = [ "orders/create" ]
100
+ // uri = "{{APP_URL}}/webhooks"
101
+ //
102
+ // 2. Add a case to the switch above:
103
+ // case "orders/create": {
104
+ // const order = body;
105
+ // // Your logic here
106
+ // break;
107
+ // }
108
+ //
109
+ // 3. Deploy: firebase deploy --only functions:webhooks
110
+ // ──────────────────────────────────────────────────────────────────────────
@@ -15,7 +15,7 @@ redirect_urls = [
15
15
  ]
16
16
 
17
17
  [webhooks]
18
- api_version = "2025-04"
18
+ api_version = "2026-01"
19
19
 
20
20
  # App lifecycle
21
21
  [[webhooks.subscriptions]]