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 +144 -39
- 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
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# create-shopify-firebase-app
|
|
2
2
|
|
|
3
|
-
>
|
|
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
|
[](https://www.npmjs.com/package/create-shopify-firebase-app)
|
|
6
6
|
[](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-
|
|
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
|
|
|
@@ -117,31 +117,35 @@ shopify app dev
|
|
|
117
117
|
|
|
118
118
|
---
|
|
119
119
|
|
|
120
|
-
## Why Firebase
|
|
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 (
|
|
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
|
|
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
|
-
| **
|
|
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 #
|
|
165
|
-
│ │ ├── auth.ts # OAuth 2.0 (
|
|
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 #
|
|
168
|
-
│ │ ├── proxy.ts # Storefront
|
|
169
|
-
│ │ ├── webhooks.ts # Webhook handlers (
|
|
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 (
|
|
204
|
+
Firebase v2 Cloud Functions (independent scaling)
|
|
199
205
|
┌─────────────────────────────────────┐
|
|
200
|
-
│
|
|
201
|
-
│
|
|
202
|
-
│
|
|
203
|
-
│
|
|
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
|
|
269
|
-
firebase deploy --only functions
|
|
270
|
-
firebase deploy --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 =
|
|
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
|
-
##
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
✔
|
|
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.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",
|
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
|
+
// ──────────────────────────────────────────────────────────────────────────
|