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 +100 -25
- package/lib/index.js +2 -1
- package/package.json +1 -1
- package/templates/firebase.json +15 -12
- package/templates/functions/package.json +8 -6
- package/templates/functions/src/admin-api.ts +16 -7
- package/templates/functions/src/auth.ts +27 -8
- package/templates/functions/src/index.ts +88 -35
- package/templates/functions/src/proxy.ts +11 -3
- package/templates/functions/src/webhooks.ts +47 -14
- package/templates/shopify.app.toml +1 -1
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-
|
|
15
|
-
<img src="https://img.shields.io/badge/Firebase-
|
|
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** (
|
|
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
|
-
- **
|
|
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 (
|
|
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
|
-
| **
|
|
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 #
|
|
165
|
-
│ │ ├── auth.ts # OAuth 2.0 (
|
|
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 #
|
|
168
|
-
│ │ ├── proxy.ts # Storefront
|
|
169
|
-
│ │ ├── webhooks.ts # Webhook handlers (
|
|
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 (
|
|
200
|
+
Firebase v2 Cloud Functions (independent scaling)
|
|
199
201
|
┌─────────────────────────────────────┐
|
|
200
|
-
│
|
|
201
|
-
│
|
|
202
|
-
│
|
|
203
|
-
│
|
|
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
|
|
269
|
-
firebase deploy --only functions
|
|
270
|
-
firebase deploy --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 =
|
|
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
|
|
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
|
-
✔
|
|
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.
|
|
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",
|
package/templates/firebase.json
CHANGED
|
@@ -3,20 +3,23 @@
|
|
|
3
3
|
"public": "web",
|
|
4
4
|
"ignore": ["firebase.json", "**/.*", "**/node_modules/**"],
|
|
5
5
|
"rewrites": [
|
|
6
|
-
{ "source": "/auth", "
|
|
7
|
-
{ "source": "/auth/**", "
|
|
8
|
-
{ "source": "/api", "
|
|
9
|
-
{ "source": "/api/**", "
|
|
10
|
-
{ "source": "/proxy", "
|
|
11
|
-
{ "source": "/proxy/**", "
|
|
12
|
-
{ "source": "/webhooks", "
|
|
13
|
-
{ "source": "/webhooks/**", "
|
|
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
|
-
|
|
18
|
-
|
|
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": "^
|
|
18
|
-
"firebase-functions": "^
|
|
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": "^
|
|
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/
|
|
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/
|
|
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
|
-
//
|
|
112
|
-
//
|
|
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
|
-
//
|
|
115
|
-
//
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 (
|
|
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
|
-
|
|
2
|
-
|
|
1
|
+
/**
|
|
2
|
+
* Cloud Function exports — each 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 {
|
|
28
|
+
import { authHandler } from "./auth";
|
|
6
29
|
import { adminApiRouter } from "./admin-api";
|
|
7
30
|
import { proxyRouter } from "./proxy";
|
|
8
|
-
import {
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
//
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
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
|
-
|
|
24
|
-
|
|
25
|
-
//
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
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
|
-
//
|
|
48
|
-
//
|
|
49
|
-
//
|
|
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:
|
|
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
|
|
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
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
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
|
-
|
|
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
|
+
// ──────────────────────────────────────────────────────────────────────────
|