This repo is a drop-in starter for Sedifex (inventory & POS). It ships as a website that is also installable as a PWA, with Firebase (Auth + Firestore + Functions).
web/ — React + Vite + TypeScript PWAfunctions/ — Firebase Cloud Functions (Node 20) with a secure commitSale transactionfirestore.rules — Multi-tenant security rules scaffold.github/workflows/ — Optional CI for deploying Functions (if you want to use GitHub Actions)Sedifex product documents now support first-class image metadata:
imageUrl?: string | null — optional public image URL (http:// or https:// only in product forms/import).imageAlt?: string | null — optional accessibility label; defaults to name when imageUrl exists and imageAlt is missing.category?: string | null — optional item grouping label used across product listings and integrations.description?: string | null — optional product summary for richer storefront rendering./api/uploads on Vercel)Sedifex now uses a real serverless API route at web/api/uploads.ts (deployed by Vercel) so the browser uploads to the same origin as the app (for example https://www.sedifex.com/api/uploads).
POST /api/uploadsfilenamemimeTypedataBase64mimeType must start with image/dataBase64 must be present and decode to non-empty bytes{ "url": "<public-image-url>" }Uploads are stored in your configured Firebase Storage bucket under the product-images/ prefix using the existing Firebase Admin credentials already used by Vercel API routes. The API returns a public Google Cloud Storage URL, which can be stored in Firestore and rendered by the Products page.
Set these in your Vercel project:
ADMIN_SERVICE_ACCOUNT_JSON (recommended) or FIREBASE_SERVICE_ACCOUNT_BASE64 (already required by existing API routes)IMAGE_UPLOAD_BUCKET (for example: sedifex-prod.appspot.com)VITE_UPLOAD_API_URL (optional; leave unset in production to use the same-origin default /api/uploads)VITE_GOOGLE_TAG_ID (optional; your GA4 Measurement ID, e.g. G-XXXXXXXXXX, to enable Google tag loading in web/index.html)Note: Firebase Functions
.env*files reserve theFIREBASE_*prefix. Use non-reserved names likeIMAGE_UPLOAD_BUCKETandADMIN_SERVICE_ACCOUNT_JSONfor deploy-safe config.
web/) to Vercel with web/ as the build root for the frontend.web/vercel.json (and root vercel.json, if used) keeps SPA rewrites from catching /api/*.POST https://www.sedifex.com/api/uploads returns 201 + { "url": "..." }Storage Object Viewer on the bucket to allUsers, or apply an equivalent public-read policy for the product-images/ path).The previous local-only
npm run upload:serverhelper has been removed to avoid confusion with production deployment.
name, price.image_urlimage_altimageUrl and imageAlt with existing storeId tenant scoping.updatedAt.listStoreProducts (Cloud Functions) for tenant-safe reads.storeId.idstoreIdnamecategorydescriptionpricestockCountitemTypeimageUrlimageAltupdatedAtdocs/integration-quickstart.md for a step-by-step guide to connect another website and auto-load products from Sedifex.docs/how-to-use-sedifex.md for an end-user tutorial (owners, cashiers, and admins) on daily Sedifex workflows.docs/integration-delivery-sequence.md for the recommended rollout order: API keys first, then WordPress plugin MVP, then product webhooks.docs/wordpress-plugin/sedifex-sync.php for a WordPress plugin MVP scaffold (settings + shortcode/block + sync health).docs/webhooks-signature-verification.md for webhook event and signature verification examples.node functions/scripts/migrateProductImageFields.js from the repo root (with Firebase admin credentials available) to backfill old records:
imageUrl = null when missingimageAlt = product.name when imageUrl exists and imageAlt is missingnpm --prefix functions run backfill-public-products -- [storeId] to copy products documents into publicProducts for public-catalog reads (optional storeId limits the backfill scope).1) Install Node 20+.
2) Go to web/ and install deps:
cd web
npm i
npm run dev
3) Create a Firebase project (e.g., sedifex-dev) and fill these env vars in web/.env.local:
VITE_FB_API_KEY=REPLACE_ME
VITE_FB_AUTH_DOMAIN=sedifex-dev.firebaseapp.com
VITE_FB_PROJECT_ID=sedifex-dev
VITE_FB_STORAGE_BUCKET=sedifex-dev.appspot.com
VITE_FB_APP_ID=REPLACE_ME
# If you manually created your default Firestore database and it uses the ID "default",
# surface it so the client targets the correct instance (falls back to "default" automatically).
VITE_FB_DATABASE_ID=default
4) (Optional) Deploy Functions:
cd functions
npm i
# Login to Firebase
npx firebase login
# Set your project
npx firebase use sedifex-dev
# Deploy
npm run deploy
localhost is allowed in dev) so the Notification API is available, and make sure web/public/sw.js, manifest.webmanifest, and the icons directory are deployed by your host. No extra env vars are required beyond the Firebase config above.activity collection. Ensure documents include storeId, type, summary, detail, actor, and a timestamp at createdAt so sorting works. Keep your Firestore rules aligned so store owners can read their activity feed; the client only requests notification permission for members with the owner role.web/ with build command npm run build and output dir dist.app.sedifex.com to the deployed frontend.Use this checklist when deploying the Google Ads integration so OAuth, callbacks, and backend syncs work correctly.
Minimum backend vars:
GOOGLE_ADS_CLIENT_IDGOOGLE_ADS_CLIENT_SECRETGOOGLE_REDIRECT_URI (recommended: https://www.sedifex.com/api/google/oauth-callback)GOOGLE_ADS_REDIRECT_URI (optional backward-compatible fallback)APP_BASE_URL (set to https://www.sedifex.com)GOOGLE_ADS_SYNC_SECRET (used by the optional manual sync endpoint)These are required by OAuth/token exchange and callback redirect logic.
Firebase Functions uses its runtime service account by default, so no extra Admin SDK JSON/base64 secret is required for deployed functions.
In your OAuth client:
https://www.sedifex.com/api/google/oauth-callbackhttps://www.googleapis.com/auth/adwordsThe backend builds the OAuth URL with that redirect URI and exchanges auth code for tokens. The URI in Google Cloud Console must be an exact string match with GOOGLE_REDIRECT_URI (or GOOGLE_ADS_REDIRECT_URI when using fallback mode).
This implementation depends on Hosting/server rewrites that forward these paths:
/api/google/oauth-start → shared OAuth start handler/api/google/oauth-callback → shared OAuth callback handler/api/google-ads/campaign → googleAdsCampaign/api/google-ads/metrics-sync → googleAdsMetricsSyncCron is provided by a scheduled Cloud Function:
googleAdsMetricsSyncScheduled (every 30 minutes)In the Ads page:
/ads and show success/failure notice.Those actions now call backend endpoints instead of client-only writes.
Server writes target:
storeSettings/{storeId}.googleAdsAutomation.*storeSettings/{storeId}.integrations.googleAds.*googleAdsOAuthStates/{hashedState}These are Admin SDK writes, so Firestore client security rules do not block them. Project health, billing status, and index/state health still matter.
Metrics sync runs every 30 minutes through:
googleAdsMetricsSyncScheduledOptional manual sync endpoint:
POST /api/google-ads/metrics-sync with x-google-ads-sync-secret: <GOOGLE_ADS_SYNC_SECRET> (or ?secret=...)https://www.sedifex.com.PAYSTACK_STANDARD_PLAN_CODE entries in runtime config, remove duplicates and keep one source of truth.firestore.rules.sedifex-prod).workspaces collection inside your primary Firestore database. Each document ID should match the workspace slug used by the app.company, contractStart, contractEnd, paymentStatus, and amountPaid to control access and billing state.Timestamp values (or ISO-8601 strings if writing via scripts), and currency values should be saved as numbers representing the smallest currency unit (e.g., cents).Seeding / maintenance steps
npx firebase login.seed/workspaces.seed.json for a ready-to-use example you can tweak per environment).npx firebase firestore:delete workspaces --project <project-id> --force && npx firebase firestore:import seed/workspaces.seed.json --project <project-id>.One-command Firestore bootstrap
From the repo root, you can refresh both collections with one command using the helper script:
node seed/firestore-seed.js --env dev # or stage | prod
The script will pick the right Firebase project ID for the chosen environment and run firestore:delete + firestore:import for both seed/workspaces.seed.json and seed/team-members.seed.json, with clear console output so you can see exactly which project is being modified.
teamMembers collection)teamMembers collection inside the default DB must contain at least one document matching the user who is attempting to sign in.uid, the verified email, and the assigned storeId. Additional helpful fields include role, name, phone, and any admin-only notes.Quick seed for local/testing environments
seed/team-members.seed.json with the UID, email, and store ID that you want to allow through login.npx firebase firestore:delete teamMembers --project <project-id> --force
npx firebase firestore:import seed/team-members.seed.json --project <project-id>
teamMembers/<uid> (and optionally teamMembers/<email>) in the default database containing the same fields as the JSON example. The login callable will reject accounts that lack both documents or that do not specify a storeId.If you create a Firebase Auth user and do not see corresponding documents in Firestore, walk through the checklist below:
onAuthCreate appears with a green check. If it is missing, redeploy from the repo root:
cd functions
npm install
npm run deploy
onAuthCreate → Logs, look for errors such as permission issues (PERMISSION_DENIED) or missing indices. Fix any issues surfaced there so the default service account can write to Firestore.Following these steps should result in new documents at teamMembers/<uid> and stores/<uid> in the default database immediately after signup.
#4338CA (indigo 700)Happy shipping! — 2025-09-23
Follow the flow below to connect Paystack as the card/mobile processor for Sedifex. The checklist assumes you already followed the Firebase setup steps above and that your stores are created by the onAuthCreate trigger.
PAYSTACK_SECRET_KEY, PAYSTACK_PUBLIC_KEY, and APP_BASE_URL (used for webhook verification and default redirects). For example:
cd functions
firebase functions:config:set PAYSTACK_SECRET_KEY="sk_live_xxx" PAYSTACK_PUBLIC_KEY="pk_live_xxx" APP_BASE_URL="https://app.sedifex.com"
stores/<storeId> document (or update your seeding script) and set paymentProvider to paystack.paymentStatus, amountPaid, contract dates) up to date so the resolveStoreAccess callable can block suspended workspaces while still returning provider info for paid or trial accounts.VITE_PAYSTACK_PUBLIC_KEY=<pk_test_or_live_value> to web/.env.local for local development and to your hosting provider for production.web/src/config/env.ts) to read the new variable and export it alongside the Firebase config.commitSale. Load Paystack’s inline widget or SDK with the amount, customer email/phone, and receive the transaction reference.payment payload with the Paystack response: e.g. { method: 'card', amountPaid, changeDue, provider: 'paystack', providerRef: response.reference, status: response.status }.commitSale already stores the payment object verbatim, so downstream reporting can access the Paystack reference without schema changes.sales records, updating statuses or flagging discrepancies for review.firebase functions:config:set paystack.secret="sk_live_..." (or your preferred secret manager) and read it in the Cloud Function that confirms transactions.sales/<id> document.firestore.rules and callable permissions so only privileged roles can change payment-related fields.resolveStoreAccess still returns billing metadata for new signups and that the UI gracefully handles both paid and trial workspaces with Paystack enabled.Documenting these steps keeps the integration consistent across environments and makes it easy to onboard additional stores with Paystack support.