create-shopify-firebase-app 1.0.0 → 1.1.0

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
@@ -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
 
@@ -121,7 +121,7 @@ shopify app dev
121
121
 
122
122
  | | `shopify app init` (Remix) | `create-shopify-firebase-app` |
123
123
  |---|---|---|
124
- | **Backend** | Remix server | Firebase Cloud Functions (Express) |
124
+ | **Backend** | Remix server (monolith) | Firebase v2 Cloud Functions (4 independent functions) |
125
125
  | **Database** | Prisma + PostgreSQL | Cloud Firestore (NoSQL, auto-scaling) |
126
126
  | **Frontend** | React + Polaris | Vanilla HTML/JS + App Bridge |
127
127
  | **Hosting** | Vercel / Fly.io / Heroku | Firebase Hosting (free tier) |
@@ -130,7 +130,7 @@ shopify app dev
130
130
  | **Deploy** | Varies | `firebase deploy` (one command) |
131
131
  | **Cost** | $5-25/month hosting | Free tier covers most apps |
132
132
  | **Framework knowledge** | Remix + React required | Express + HTML (that's it) |
133
- | **Backend code** | ~2000+ lines (framework) | ~350 lines (you own all of it) |
133
+ | **Scaling** | Single server | Per-function auto-scaling (Cloud Run) |
134
134
  | **GDPR webhooks** | Auto-handled | Included (ready for App Store) |
135
135
  | **Theme extensions** | Supported | Supported (same Shopify format) |
136
136
  | **Shopify Functions** | Supported | Supported (add via Shopify CLI) |
@@ -155,18 +155,18 @@ shopify app dev
155
155
 
156
156
  ```
157
157
  my-app/
158
- ├── shopify.app.toml # Shopify app config
158
+ ├── shopify.app.toml # Shopify app config (API 2026-01)
159
159
  ├── firebase.json # Firebase Hosting + Functions + Firestore
160
160
  ├── firestore.rules # Security rules (blocks direct client access)
161
161
 
162
- ├── functions/ # ── Backend ──
162
+ ├── functions/ # ── Backend (4 Cloud Functions) ──
163
163
  │ ├── src/
164
- │ │ ├── index.ts # Express app + Cloud Function export
165
- │ │ ├── auth.ts # OAuth 2.0 (install + callback + token storage)
164
+ │ │ ├── index.ts # Function exports (auth, api, webhooks, proxy)
165
+ │ │ ├── auth.ts # OAuth 2.0 (standalone no Express)
166
166
  │ │ ├── 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)
167
+ │ │ ├── admin-api.ts # Admin dashboard API routes (Express)
168
+ │ │ ├── proxy.ts # Storefront App Proxy routes (Express)
169
+ │ │ ├── webhooks.ts # Webhook handlers (standalone no Express)
170
170
  │ │ ├── firebase.ts # Firebase Admin SDK init
171
171
  │ │ └── config.ts # Environment config
172
172
  │ └── .env # Your secrets (auto-generated, git-ignored)
@@ -187,6 +187,8 @@ my-app/
187
187
 
188
188
  ## Architecture
189
189
 
190
+ Each function scales independently on Cloud Run (Firebase v2 / gen 2):
191
+
190
192
  ```
191
193
  Shopify Admin (iframe)
192
194
  ┌─────────────────────────────────────┐
@@ -195,12 +197,12 @@ my-app/
195
197
  └──────────────┬──────────────────────┘
196
198
  │ Bearer <JWT>
197
199
 
198
- Firebase Cloud Functions (Express)
200
+ Firebase v2 Cloud Functions (independent scaling)
199
201
  ┌─────────────────────────────────────┐
200
- /auth → OAuth 2.0 flow
201
- /api/* → Admin API (JWT auth)
202
- /proxy/* App Proxy (HMAC) │
203
- /webhooks Webhooks (HMAC)
202
+ │ auth() → OAuth 2.0 flow (standalone, no Express)
203
+ │ api() → Admin API (JWT) │ (Express + middleware)
204
+ webhooks() Webhooks (HMAC) │ (standalone, no Express)
205
+ proxy() App Proxy (HMAC) (Express)
204
206
  └──────────────┬──────────────────────┘
205
207
 
206
208
 
@@ -212,6 +214,16 @@ my-app/
212
214
  └─────────────────────────────────────┘
213
215
  ```
214
216
 
217
+ ### Why split functions?
218
+
219
+ | Benefit | How |
220
+ |---------|-----|
221
+ | **Faster webhooks** | `webhooks()` has no Express overhead — responds in milliseconds |
222
+ | **Independent scaling** | Each function auto-scales based on its own traffic |
223
+ | **Targeted deploys** | `firebase deploy --only functions:api` deploys just one function |
224
+ | **Separate configs** | Each function gets its own memory, timeout, and concurrency |
225
+ | **Lower cold starts** | Smaller functions = faster cold starts |
226
+
215
227
  ### Three Security Layers
216
228
 
217
229
  | Layer | Protects | How |
@@ -265,15 +277,56 @@ shopify app dev
265
277
  ### Deploy
266
278
 
267
279
  ```bash
268
- firebase deploy # Everything
269
- firebase deploy --only functions # Backend only
270
- firebase deploy --only hosting # Frontend only
280
+ firebase deploy # Everything
281
+ firebase deploy --only functions # All functions
282
+ firebase deploy --only functions:auth # Just auth function
283
+ firebase deploy --only functions:api # Just API function
284
+ firebase deploy --only hosting # Frontend only
271
285
  ```
272
286
 
273
287
  ---
274
288
 
275
289
  ## Extending Your App
276
290
 
291
+ ### Add a new Cloud Function
292
+
293
+ This is the most common extension pattern. Create a new file, export a handler, and wire it up:
294
+
295
+ **1. Create `functions/src/my-feature.ts`:**
296
+
297
+ ```typescript
298
+ import type { Request, Response } from "firebase-functions/v2/https";
299
+ import { db } from "./firebase";
300
+
301
+ export async function myFeatureHandler(req: Request, res: Response) {
302
+ // Your logic here
303
+ res.json({ success: true });
304
+ }
305
+ ```
306
+
307
+ **2. Export it in `functions/src/index.ts`:**
308
+
309
+ ```typescript
310
+ import { myFeatureHandler } from "./my-feature";
311
+
312
+ export const myFeature = onRequest(
313
+ { memory: "256MiB", timeoutSeconds: 60 },
314
+ myFeatureHandler,
315
+ );
316
+ ```
317
+
318
+ **3. Add a rewrite in `firebase.json`:**
319
+
320
+ ```json
321
+ { "source": "/my-feature/**", "function": "myFeature" }
322
+ ```
323
+
324
+ **4. Deploy just your function:**
325
+
326
+ ```bash
327
+ firebase deploy --only functions:myFeature
328
+ ```
329
+
277
330
  ### Add admin API routes
278
331
 
279
332
  Edit `functions/src/admin-api.ts`:
@@ -304,7 +357,7 @@ Edit `functions/src/webhooks.ts` and register in `shopify.app.toml`:
304
357
 
305
358
  ```typescript
306
359
  case "orders/create": {
307
- const order = req.body;
360
+ const order = body;
308
361
  // Your logic
309
362
  break;
310
363
  }
@@ -325,6 +378,27 @@ Create `web/settings.html`, use the same pattern, navigate with `App.navigate("/
325
378
 
326
379
  Use the `appSubscriptionCreate` GraphQL mutation in your admin API routes.
327
380
 
381
+ ### Add a scheduled function (cron)
382
+
383
+ ```typescript
384
+ // functions/src/cleanup.ts
385
+ import { onSchedule } from "firebase-functions/v2/scheduler";
386
+ import { db } from "./firebase";
387
+
388
+ export const dailyCleanup = onSchedule("every 24 hours", async () => {
389
+ // Clean up expired nonces, old data, etc.
390
+ const cutoff = new Date(Date.now() - 48 * 60 * 60 * 1000);
391
+ const old = await db.collection("authNonces")
392
+ .where("createdAt", "<", cutoff.toISOString()).get();
393
+ for (const doc of old.docs) await doc.ref.delete();
394
+ });
395
+ ```
396
+
397
+ Export it in `index.ts`:
398
+ ```typescript
399
+ export { dailyCleanup } from "./cleanup";
400
+ ```
401
+
328
402
  ---
329
403
 
330
404
  ## Firebase Free Tier
@@ -364,8 +438,9 @@ These are **required** for Shopify App Store listing.
364
438
  | "Missing shop parameter" | Set **App URL** in Partner Dashboard to `https://PROJECT_ID.web.app` |
365
439
  | "HMAC verification failed" | Check `SHOPIFY_API_SECRET` in `functions/.env` |
366
440
  | "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` |
441
+ | Functions not receiving requests | Check `firebase.json` rewrites match function export names in `index.ts` |
368
442
  | Webhook failures | Must respond 200 within 5 seconds. Check `firebase functions:log` |
443
+ | Individual function not deploying | Ensure export name in `index.ts` matches function name in `firebase.json` rewrite |
369
444
 
370
445
  ---
371
446
 
@@ -385,7 +460,7 @@ npm link # Test locally: create-shopify-firebase-app test-app
385
460
  ## Related
386
461
 
387
462
  - [Shopify App Development](https://shopify.dev/docs/apps) — Official docs
388
- - [Firebase Cloud Functions](https://firebase.google.com/docs/functions) — Backend runtime
463
+ - [Firebase v2 Cloud Functions](https://firebase.google.com/docs/functions) — Backend runtime (gen 2)
389
464
  - [Shopify App Bridge](https://shopify.dev/docs/api/app-bridge) — Embedded app SDK
390
465
  - [Shopify Admin GraphQL API](https://shopify.dev/docs/api/admin-graphql) — Store data API
391
466
  - [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.0",
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]]