devsnack
Documentation

Feastly documentation

Run it, point it at your own accounts, ship it under your own brand. Written against version 1.1.0 of the download you get on CodeCanyon.

$39 on CodeCanyon
  • Version v1.1.0
  • Updated Aug 2026
  • Platform Flutter
  • Stack Flutter · Next.js
On this page
01

Introduction

A resellable, multi-restaurant on-demand food-delivery platform shipped as complete source code: three Flutter apps (Customer, Rider, Restaurant) and a Next.js admin panel on one shared backend, monetized by per-order commission, with an optional AI search & recommendations layer.

Feastly is a complete, self-hostable food-delivery platform you can rebrand and launch in your own market — an UberEats / Grab / Foodpanda-style operation. It is a four-surface bundle on a single shared backend:

SurfaceTechWho uses it
Customer appapps/customer
Flutter
Browse restaurants, order, pay, track delivery live.
Rider appapps/rider
Flutter
Go online, accept orders, advance the delivery, track earnings.
Restaurant appapps/restaurant
Flutter
Manage the menu, run the order board, view payouts.
Admin panel + backendapps/web
Next.js
The platform owner (you / your buyer): onboarding, commission, oversight — and the shared API, AI, and realtime backend all three apps run against.

The wedge. The food-delivery category is saturated with PHP/Laravel incumbents. Feastly competes on a full 4-app ecosystem, a modern Next.js + Flutter stack, an optional dual-provider AI layer, and zero paid-service lock-in for the buyer: the realtime layer is self-hosted, transactional email is plain SMTP, and AI is fully optional.

Portable. The backend is one Next.js app you can deploy to Vercel or any Node host / VPS / container. The database is standard PostgreSQL (the docs use Neon’s free serverless tier; any Postgres works). No secret ever ships inside a mobile binary.

What’s included

  • Full source for all four surfaces — three Flutter apps + the Next.js admin/API/AI/realtime backend.
  • Shared Drizzle schema (24 tables) with 3 generated SQL migrations, runnable on a fresh Postgres.
  • A shared Dart package (packages/shared-dart) — theme, typed API client, and models used by all three apps.
  • An idempotent demo seed: 12 restaurants, 71 menu items, 11 users, 11 orders (5 completed + 6 in-flight), commission & payout ledgers, ratings, cuisines, banners and a delivery zone — so no surface ever loads empty.
  • The design system (design tokens, Inter typography, the “AI Spark” gradient marker) mirrored across web and mobile.
  • A branding pipeline: brand SVG/PNG masters + one-command launcher-icon regeneration.
  • Optional, gracefully-degrading integrations: Stripe, Cloudflare R2, SMTP, Google Maps, OpenAI/Gemini.
  • This documentation, a quick-start guide, per-surface widget tests, and a dependency license audit.
  • 6 months of item support (see Support and licensing).
02

Features

Customer app

  • Email/password onboarding (JWT-backed mobile sessions); manual address book with delivery-zone gating.
  • Discovery: restaurant list, categories, cuisines, filters & sort, and AI-assisted search (degrades to full-text).
  • Restaurant page with categorised menu, item detail, modifiers/add-ons, and veg/allergen tags.
  • Cart & checkout: modifiers, order notes, tip, delivery vs pickup, address & payment-method select.
  • Payments: Cash on Delivery (first-class) and card via Stripe (webhook-confirmed).
  • Live order tracking: status timeline + rider location over the self-hosted realtime stream.
  • Post-order: rate restaurant + rider, reorder, order history.
  • Recommendations: “Recommended for you” / “Popular near you” (degrades to popularity).

Rider app

  • Rider auth & profile; online/offline availability toggle.
  • Order intake: auto-assign or accept-from-pool (admin-configurable mode).
  • Full status flow: accepted → arrived → picked up → en route → delivered, with a location ping.
  • Earnings: per-order breakdown plus daily/weekly totals and payout status.

Restaurant app

  • Restaurant auth & profile (hours, min order).
  • Menu management: categories, items, modifiers, availability toggles, images.
  • Order board: incoming → accept/reject → preparing → ready-for-pickup.
  • Earnings dashboard: gross, commission deducted, net owed.
  • Basic analytics: orders, revenue, top items.

Admin panel (Next.js)

  • Dashboard: GMV, orders, commission earned, top items.
  • Restaurant management: onboard/approve, per-restaurant or global commission %, suspend.
  • Rider management: onboard/approve, assignment mode, suspend.
  • Order oversight: search, live status, manual reassign, refund/cancel.
  • Commission engine: global default + per-restaurant override, automatic per-order calculation into an immutable ledger.
  • Payout ledger: per-restaurant and per-rider amounts owed, with mark-as-paid (manual settlement in V1).
  • Content: banners, categories, cuisines, delivery zones (geofence editor) at /admin/content.
  • Configuration: payment / AI / maps / SMTP / push keys — all stored server-side.
  • Users, roles, disputes, and ratings moderation.

Backend API (all surfaces run against it)

42 route handlers under apps/web/src/app/api, including /api/mobile/* (bootstrap config, JWT auth, addresses, zone-check), /api/restaurants, /api/search, /api/recommendations, /api/orders, /api/payments/webhook/[gateway], /api/uploads/presign, /api/realtime/* (SSE), and /api/inngest (background jobs).

03

Architecture

How the four surfaces connect

The three Flutter apps and the browser-based admin UI all talk to one shared Next.js backend (apps/web). Mobile apps use HTTPS REST with a JWT bearer and receive live updates over SSE; the admin UI is served by the same app using session cookies. Every third-party key stays server-side.

Flows. Mobile → backend over HTTPS REST (JWT bearer); backend → mobile live updates over SSE (order status + rider location); the admin browser ↔ backend over session cookies; backend ↔ PostgreSQL via Drizzle ORM. Payment confirmation arrives as a signature-verified webhook before an order advances past payment. All optional third-party keys are read server-side and never ship inside a mobile binary.

Monorepo layout

feastly/
feastly/
├── apps/
│   ├── web/            # Next.js — admin UI + API + AI + realtime (SSE) backend
│   ├── customer/       # Flutter
│   ├── rider/          # Flutter
│   └── restaurant/     # Flutter
├── packages/
│   ├── config/         # shared TS config + Tailwind design-token preset
│   ├── db/             # Drizzle schema + migrations (PostgreSQL / Neon)
│   └── shared-dart/    # shared Dart: theme, API client, models
├── brand/              # logo SVG/PNG masters → launcher icons
├── scripts/            # set-maps-key.sh
└── .env.example        # every environment variable, grouped

The JS side (apps/web, packages/config, packages/db) is a Turborepo + pnpm workspace. The three Flutter apps and packages/shared-dart are a Dart pub workspace (Melos optional).

Tech stack

Web / admin / API
Next.js 15 (App Router, React 19), TypeScript
Database / ORM
PostgreSQL (Neon serverless, HTTP driver) + Drizzle ORM
Auth
Auth.js v5 (session cookies for admin) + JWT bearer for mobile (jose), bcrypt password hashing
Background jobs
Inngest (order-state jobs, notification fan-out, ledger events)
Media
Cloudflare R2 via the S3-compatible SDK (presigned uploads)
Email
nodemailer / SMTP
Realtime
Self-hosted in-process pub/sub over SSE (no Firebase)
AI
Provider abstraction over OpenAI + Gemini embeddings (server-side only)
Mobile
Flutter 3.24+ / Dart 3.12, Riverpod, GoRouter, freezed, dio, flutter_secure_storage, google_fonts
Payments
Stripe SDK (server) + flutter_stripe (customer app); COD via a local manual gateway
Styling
Tailwind CSS (web) + a shared design-token preset; Material 3 theme (mobile)

Design invariants

  • All third-party keys are server-side only and admin-configurable — no secret ships in an APK/IPA.
  • AI is fully optional: the app is 100% functional with AI disabled. Model IDs are env / admin-config toggled, never hardcoded.
  • Money math is fail-safe (reserve → commit → refund); an order only advances past payment after a webhook-confirmed charge. COD is first-class.
  • The order lifecycle is a canonical state machine; every transition emits a notification + a background event; the commission ledger entry written on completion is immutable.
  • Graceful empty/offline states everywhere; single-currency config and an i18n scaffold in V1.
04

Prerequisites

ToolVersionNotes
Node
24.x (≥ 22 works)
the repo pins 24 via .nvmrc / engines
pnpm
10.x
corepack enable
Flutter / Dart
3.24+ / 3.12+
for the three mobile apps
PostgreSQL
15+
any Postgres; the docs use a free Neon project

No paid third-party service is required. Payments, AI, Maps, Push and SMTP are all optional (Optional integrations).

05

Local run

terminal
# 1. install JS dependencies (repo root)
pnpm install

# 2. configure environment — copy the template and set the two required values
cp .env.example .env
#   DATABASE_URL   = your Postgres/Neon connection string
#   AUTH_SECRET    = "$(openssl rand -base64 32)"

# 3. create the schema + demo data
pnpm db:migrate
pnpm db:seed

# 4. run the backend + admin panel  → http://localhost:3000
pnpm --filter @feastly/web dev

# 5. resolve the Dart workspace, then run each app
flutter pub get
cd apps/customer   && flutter run
cd apps/rider      && flutter run
cd apps/restaurant && flutter run
Note
Mobile backend URL. The apps default to the production backend https://feastly.devsnack.dev (ApiConfig.productionBaseUrl in packages/shared-dart/lib/src/api/api_config.dart). Point them anywhere else without editing code: flutter run --dart-define=FEASTLY_API_URL=http://10.0.2.2:3000 (the Android emulator’s route to your host’s localhost).
06

Deployment

Deploy the backend — Vercel

  1. 01
    Push the repo to GitHub, then New Project → Import in Vercel.
  2. 02
    Set Root Directory to apps/web (the one critical setting). Framework (Next.js), build (next build) and install (pnpm install) auto-detect. Node 24.x is pinned via engines.
  3. 03
    Add environment variables (Production): required DATABASE_URL, AUTH_SECRET, AUTH_TRUST_HOST=true, AUTH_URL=https://your-domain; plus any optional keys you use (Optional integrations).
  4. 04
    Add your custom domain and keep AUTH_URL in sync with it.
  5. 05

    Run migrations once from your machine against the production DB (Vercel does not run them):

    terminal
    DATABASE_URL="<prod-url>" pnpm db:migrate

Deploy the backend — any Node host / VPS / container

terminal
pnpm install
pnpm --filter @feastly/web build
DATABASE_URL="…" AUTH_SECRET="…" pnpm --filter @feastly/web start   # serves on :3000

Set DATABASE_URL and AUTH_SECRET in the host’s environment. Run pnpm db:migrate once against the production database.

Warning
Realtime on serverless. The realtime layer (apps/web/src/lib/realtime.ts) is an in-process pub/sub — perfect on a single Node instance, but on Vercel’s serverless functions live tracking won’t propagate across instances (the tracking screen still shows status from its initial fetch and on refresh). For true realtime at scale, back publish()/subscribe() with Redis pub/sub behind the same interface, or run apps/web on a persistent Node host.

Build the mobile apps

terminal
flutter build apk    --dart-define=FEASTLY_API_URL=https://your-domain   # Android
flutter build appbundle --dart-define=FEASTLY_API_URL=https://your-domain
flutter build ios    --dart-define=FEASTLY_API_URL=https://your-domain   # iOS (from macOS)
07

Database setup

  1. 01

    Provision Postgres

    Create a free project at neon.tech (or use any Postgres) and copy the connection string.
  2. 02

    Set env

    Put it in .env as DATABASE_URL (a single root .env feeds both the web app and the DB tooling).
  3. 03

    Migrate

    pnpm db:migrate applies the 3 committed SQL migrations. (Changed the schema? pnpm db:generate regenerates SQL first.)
  4. 04

    Seed

    pnpm db:seed loads the demo data. The seed is idempotent — if customer@feastly.dev already exists it logs and exits cleanly, so it is safe to re-run and will not duplicate.

The schema is split into auth, catalog, orders, ledger and system groups under packages/db/src/schema:

Auth
users, accounts, sessions, verification_tokens, addresses
Catalog
restaurants, riders, menu_categories, menu_items, modifier_groups, modifiers
Orders
orders, order_items, payments, ratings, disputes
Ledger
commission_ledger, payout_ledger
System
cuisines, delivery_zones, banners, notifications, app_config, audit_log
Warning
Do not seed a real production database. The seed is for demo/staging. Migrate production, then let real data flow in.
08

Backups and exports

Your whole platform — restaurants, menus, orders, ledgers, users — lives in one PostgreSQL database (whatever DATABASE_URL points at). You don’t need to know PostgreSQL internals to keep it safe: a backup is a single command. The two tools below, pg_dump and pg_restore/psql, ship with PostgreSQL — install them with brew install libpq on macOS or apt install postgresql-client on Linux.

Back up the entire database to one file

pg_dump writes a complete, restorable snapshot. The compressed custom format is the safest default:

terminal
# everything → one compressed file
pg_dump "$DATABASE_URL" --format=custom --file=feastly-backup.dump

# …or a plain-text .sql file you can open and read
pg_dump "$DATABASE_URL" --format=plain --file=feastly-backup.sql

Keep that file somewhere safe (your computer, an R2/S3 bucket, a private repo). That one file is your full backup.

Restore into a fresh database

Point at an empty database ($TARGET_DATABASE_URL) and load the dump back in:

terminal
# from a custom-format .dump
pg_restore --clean --if-exists --no-owner --dbname "$TARGET_DATABASE_URL" feastly-backup.dump

# from a plain .sql file
psql "$TARGET_DATABASE_URL" --file=feastly-backup.sql

Export a single table to CSV (e.g. for a spreadsheet)

terminal
psql "$DATABASE_URL" --command "\copy (SELECT * FROM orders) TO 'orders.csv' WITH CSV HEADER"
Note
Prefer a dashboard? Most managed Postgres hosts back up for you — no command line needed. On Neon (the free tier these docs use) every project has automatic point-in-time restore plus one-click branching to snapshot the database; other hosts offer a “Backups”, “Snapshots” or “Export” button. The pg_dump command above still works on all of them and is the portable way to move data between providers.
Note
Reset the demo data any time by re-running the idempotent seed: pnpm db:migrate && pnpm db:seed. For production, schedule the pg_dump command as a nightly cron job or GitHub Action and retain the last several dumps.
09

Optional integrations

Every integration is optional and gracefully no-ops when its keys are blank — the app stays fully functional. Keys live in .env or, at runtime, in the admin Configuration page; the code resolves env first, then the admin store.

Warning
Third-party services & costs. Stripe, Cloudflare R2, OpenAI/Gemini, Google Maps, SMTP and FCM are independent third-party services you sign up for and connect directly — none is included in, bundled with, or resold through this item. Each is governed by that provider’s own terms and pricing and may incur costs billed to you by that provider. Feastly runs with all of them disabled (COD, self-hosted realtime, local/full-text fallbacks); enable only what you need.
IntegrationUnlocksEnv varsKeyless behavior
Payments — Stripewired
Card payments, webhook-confirmed refunds
STRIPE_SECRET_KEY, STRIPE_WEBHOOK_SECRET, STRIPE_PUBLISHABLE_KEY
Falls back to COD-only via the local manual gateway; paymentsEnabled=false in the mobile bootstrap config.
Payments — Razorpay / PayPalroadmap
Additional card rails
RAZORPAY_*, PAYPAL_*
Env slots reserved; only Stripe + the manual/COD gateway ship adapters today.
Media — Cloudflare R2optional
Menu/banner image uploads (presigned PUT)
R2_ACCOUNT_ID, R2_ACCESS_KEY_ID, R2_SECRET_ACCESS_KEY, R2_BUCKET, R2_PUBLIC_URL
presignUpload() returns null → the presign route replies 503; uploads are disabled. Existing/seeded imagery is unaffected.
AI — OpenAI / Geminioptional
Semantic search + personalized recommendations
AI_PROVIDER, OPENAI_API_KEY + OPENAI_EMBEDDING_MODEL, or GEMINI_API_KEY + GEMINI_EMBEDDING_MODEL
Provider resolves to null → search degrades to full-text, recommendations to popularity/proximity.
Email — SMTPoptional
Transactional order emails
SMTP_HOST, SMTP_PORT, SMTP_USER, SMTP_PASSWORD, SMTP_FROM
sendEmail() is a clean no-op (logs a debug line, sends nothing).
Push — FCMscaffolded
Per-transition push notifications
FCM_PROJECT_ID, FCM_CLIENT_EMAIL, FCM_PRIVATE_KEY
Env slots reserved; server-side FCM delivery is not wired yet. Notifications are persisted in-DB and delivered in-app over the realtime stream.
Maps — Googleoptional
Admin geofence editor + (mobile map, once wired)
NEXT_PUBLIC_GOOGLE_MAPS_API_KEY (web), MAPS_API_KEY (mobile build-injected), GOOGLE_MAPS_SERVER_KEY
The admin zone editor shows a setup notice; nothing else breaks.
Realtimebuilt-in
Live order status + rider location
REDIS_URL (only for multi-node)
Self-hosted SSE; single-node out of the box. Blank REDIS_URL = in-process pub/sub (correct for dev and any single instance); set it to fan events across instances (Payments, media, push and realtime).
10

Environment reference

Every variable the platform reads, in one place. These tables mirror .env.example line-for-line — that file is the canonical template, so cp .env.example .env always gives you a complete, current file. The same key list also drives the admin Configuration page (apps/web/src/lib/config.ts), so template, docs and admin UI stay in step; when a release adds a variable it lands in all three.

Only the first two are required. Every other row can stay blank — the “If left blank” column tells you exactly what happens.

Required

VariablePurposeIf left blank
DATABASE_URL
PostgreSQL connection string (Neon or any Postgres)
App cannot start — this is required.
AUTH_SECRET
Auth.js session-signing secret — openssl rand -base64 32
Sign-in fails — this is required.

Core URLs (set these in production)

VariablePurposeIf left blank
AUTH_URL
Canonical site URL Auth.js issues callbacks against
Defaults to localhost; set it to your domain in production.
AUTH_TRUST_HOST
true when running behind a proxy or on Vercel
Auth may reject proxied hosts — set true on Vercel.
NEXT_PUBLIC_APP_URL
Public base URL used for absolute links in the admin panel
Falls back to localhost; relative links still work.

Payments and currency

VariablePurposeIf left blank
STRIPE_SECRET_KEY
Server-side Stripe key — payment intents & refunds
Stripe adapter stays off; the app runs COD-only through the built-in manual gateway.
STRIPE_WEBHOOK_SECRET
Verifies Stripe webhook signatures
Stripe adapter stays off; the app runs COD-only through the built-in manual gateway.
STRIPE_PUBLISHABLE_KEY
Public key served to the mobile apps (public by design)
Stripe adapter stays off; the app runs COD-only through the built-in manual gateway.
RAZORPAY_KEY_ID, RAZORPAY_KEY_SECRET
Reserved slots for a Razorpay adapter
No effect today — adapters are roadmap; the slots exist so adding one needs no schema change.
PAYPAL_CLIENT_ID, PAYPAL_CLIENT_SECRET
Reserved slots for a PayPal adapter
No effect today — adapters are roadmap; the slots exist so adding one needs no schema change.
CURRENCY
ISO 4217 code for your single market, e.g. USD
Defaults to USD. Served to mobile via /api/mobile/config.

Media — Cloudflare R2

VariablePurposeIf left blank
R2_ACCOUNT_ID, R2_ACCESS_KEY_ID, R2_SECRET_ACCESS_KEY, R2_BUCKET, R2_PUBLIC_URL
Presigned direct-to-bucket image uploads for menus & banners
Uploads disabled (presign returns 503). Existing and seeded imagery is unaffected.

Push — Firebase Cloud Messaging

VariablePurposeIf left blank
FCM_PROJECT_ID, FCM_CLIENT_EMAIL, FCM_PRIVATE_KEY
Service-account credentials for native push
No native push. Notifications are still written to the database and delivered in-app over the realtime stream.

AI (fully optional)

VariablePurposeIf left blank
AI_PROVIDER
openai, gemini, or blank to disable
AI off: search runs full-text, recommendations run on popularity/proximity. The app is 100% functional.
OPENAI_API_KEY
OpenAI credential (server-side only)
OpenAI provider unavailable; see AI_PROVIDER above.
OPENAI_EMBEDDING_MODEL
Embedding model ID — powers semantic search & recommendations
OpenAI provider unavailable; see AI_PROVIDER above.
OPENAI_CHAT_MODEL
Chat model ID — reserved slot, not used by V1 (V1 uses embeddings only)
No effect in V1.
GEMINI_API_KEY
Google Gemini credential (server-side only)
Gemini provider unavailable; see AI_PROVIDER above.
GEMINI_EMBEDDING_MODEL
Gemini embedding model ID
Gemini provider unavailable; see AI_PROVIDER above.
GEMINI_CHAT_MODEL
Gemini chat model ID — reserved slot, not used by V1
No effect in V1.

Transactional email — SMTP

VariablePurposeIf left blank
SMTP_HOST, SMTP_PORT, SMTP_USER, SMTP_PASSWORD, SMTP_FROM
Any SMTP provider — no paid mail service is locked in
sendEmail() is a clean no-op: it logs and sends nothing. Nothing else breaks.

Maps

VariablePurposeIf left blank
NEXT_PUBLIC_GOOGLE_MAPS_API_KEY
Maps JS key for the admin geofence editor (public by design — restrict it by HTTP referrer). Inlined at build time, so it must be set before you build the admin panel.
The geofence editor shows a setup notice; nothing else breaks.
GOOGLE_MAPS_SERVER_KEY
Server-side key for geocoding / distance lookups
Server-side map lookups are skipped.

Realtime at scale

VariablePurposeIf left blank
REDIS_URL
Redis pub/sub backing for multi-instance realtime (Payments, media, push and realtime)
In-process pub/sub — correct for dev and any single Node instance.

Set at build time, not in .env

Two values belong to the mobile build rather than the server environment:

ValueWhere it goesDefault
FEASTLY_API_URL
flutter build … --dart-define=FEASTLY_API_URL=https://your-domain
https://feastly.devsnack.dev — change this to your own backend before you ship.
MAPS_API_KEY
Gitignored android/local.properties + ios/Flutter/Maps.xcconfig, or scripts/set-maps-key.sh
Empty. Never committed, never inside the tracked source.
Note
Rule of thumb. Anything secret lives in the server environment or the admin Configuration page — never in a mobile binary. The mobile apps only ever fetch publishable values from /api/mobile/config at runtime.
11

AI setup

The AI layer (apps/web/src/lib/ai/provider.ts) is a server-side provider abstraction over OpenAI and Gemini embeddings. It powers two surfaces, both badged with the “AI Spark” marker:

  • AI-assisted search (/api/search) — embedding-based semantic ranking with a keyword fallback.
  • Recommendations (/api/recommendations) — “Recommended for you” / “Popular near you” from order history, falling back to popularity/proximity on cold start.

Enable it

.env
AI_PROVIDER="openai"                 # or "gemini", or "" to disable
OPENAI_API_KEY="sk-…"
OPENAI_EMBEDDING_MODEL="text-embedding-3-small"
# Gemini instead:
# AI_PROVIDER="gemini"
# GEMINI_API_KEY="…"
# GEMINI_EMBEDDING_MODEL="…"
  • Server-only. Keys are read from env or the admin config store and are never returned to a client or embedded in a mobile binary.
  • Never hardcoded. Model IDs are configuration values, so you can swap models without a code change.
  • Fully optional. Leave AI_PROVIDER blank and every AI surface degrades gracefully — the app is 100% functional with AI off.
Warning
Before production: the AI endpoints have no built-in per-user/IP rate limiter yet. If you enable AI on a public deployment, add a rate limit (e.g. an edge middleware or a Redis token bucket) in front of /api/search and /api/recommendations to cap embedding spend. This is a documented hardening step, not a default.
12

Payments, media, push and realtime

Payments

A CardGateway interface (apps/web/src/lib/payments.ts) has two shipping implementations: a manual gateway (the default; COD is first-class, and card flows confirm via a local webhook) and a Stripe gateway (payment intents, refunds, and signature-verified webhooks). The order state machine only advances past payment on a webhook-confirmed charge. Webhooks land at /api/payments/webhook/[gateway]. The mobile apps fetch only publishable config from /api/mobile/config — the Stripe secret and webhook secret are never sent to a client.

Media (Cloudflare R2)

Image uploads use short-lived presigned PUT URLs minted at /api/uploads/presign — the client uploads directly to R2 and only ever receives the presigned URL plus the resulting public URL. With R2 unconfigured, presigning returns null and uploads are simply disabled; existing/seeded imagery is unaffected. Set the five R2_* vars to enable.

Push notifications

Order transitions write a notifications row and publish a realtime event, so every surface reflects status changes in-app immediately. Native FCM delivery is scaffolded (env slots present) but not yet wired server-side — enabling true push is a documented roadmap step, not required for the flow to work.

Background jobs (Inngest)

Inngest handles order-state jobs, notification fan-out and ledger events at /api/inngest. It runs against Inngest’s dev server locally and their cloud (or a self-hosted relay) in production.

Realtime

The self-hosted SSE layer streams order status and rider location to the customer tracking screen and the restaurant/rider/admin boards (/api/realtime/*). Out of the box it is an in-process pub/sub (apps/web/src/lib/realtime.ts) — perfect for dev and a single Node instance. On a horizontally-scaled or serverless deploy, an event published on one instance won’t reach SSE clients connected to another.

Scaling out: a Redis pub/sub adapter

Back the bus with Redis and every instance shares one channel — the publish() / subscribe() surface the rest of the app calls stays identical, so no route, job or component changes. Install the client and point REDIS_URL at any Redis (Upstash, Redis Cloud, or your own):

terminal
pnpm --filter @feastly/web add ioredis
# .env  →  REDIS_URL=redis://default:••••@your-host:6379

Then swap the body of apps/web/src/lib/realtime.ts for this drop-in replacement — same exports, Redis-backed fan-out:

apps/web/src/lib/realtime.ts
import 'server-only';
import Redis from 'ioredis';

export type RealtimeEvent = { type: 'status' | 'location' | 'hello' | 'order' | 'pool'; data: unknown };
type Listener = (event: RealtimeEvent) => void;

export const CHANNELS = {
  restaurant: (id: string) => `restaurant:${id}`,
  adminOrders: 'admin:orders',
  riderPool: 'rider:pool',
} as const;

const BUS = 'feastly:realtime';                    // one shared Redis channel
const local = new Map<string, Set<Listener>>();     // channel → this node's SSE listeners

// A connection in subscribe mode can't run other commands, so publishing needs its own.
const pub = new Redis(process.env.REDIS_URL!);
const sub = new Redis(process.env.REDIS_URL!);

// Fan every bus message out to the listeners living on THIS node.
sub.subscribe(BUS);
sub.on('message', (_channel, payload) => {
  const { channel, event } = JSON.parse(payload) as { channel: string; event: RealtimeEvent };
  const set = local.get(channel);
  if (!set) return;
  for (const listener of set) {
    try { listener(event); } catch { /* a dead listener must not break the fan-out */ }
  }
});

export function subscribe(channel: string, listener: Listener): () => void {
  let set = local.get(channel);
  if (!set) local.set(channel, (set = new Set()));
  set.add(listener);
  return () => {
    set!.delete(listener);
    if (set!.size === 0) local.delete(channel);
  };
}

export function publish(channel: string, event: RealtimeEvent): void {
  // Publish to Redis only; the message loops back through sub.on('message')
  // above, so this node's listeners fire exactly once — same path as every other node.
  pub.publish(BUS, JSON.stringify({ channel, event }));
}

Why publish only to Redis? Every node — including the one that called publish() — receives the event back through the shared subscriber, so each listener fires exactly once with no special-casing of the origin. For a managed option, Upstash exposes a Redis-compatible endpoint that works from serverless functions.

13

Rebrand step by step

Five edits turn Feastly into your brand — roughly half an hour, no screen-by-screen work. Each step names the exact file to change, and the paths below are copy-paste ready. Steps 1–4 are the visual identity; step 5 points the apps at your backend and legal pages. Finish with the verification checklist to confirm nothing was missed.

  1. 01

    Swap the logo and app icon

    Replace the two 1024×1024 master PNGs with your own square icon art — keep the same file names and dimensions and everything downstream regenerates from them: brand/icon-1024.png, brand/foreground-1024.png.

    Optional vector sources: brand/feastly-icon.svg, brand/feastly-mark.svg. Web favicon & admin browser icon: apps/web/src/app/favicon.ico, apps/web/src/app/icon.svg, apps/web/src/app/apple-icon.png.

  2. 02

    Update the color scheme

    The palette is defined once per platform from a single primary color — change it and every screen, button and badge follows. Current tokens:

    primary
    #ae3200
    primary-container
    #ff5a1f
    tertiary
    #006d37
    error
    #ba1a1a
    surface
    #f8f9fa

    Web / admin: packages/config/tailwind-preset.ts (the shared Tailwind design-token preset). Mobile (all three apps at once): packages/shared-dart/lib/src/theme/app_colors.dart and packages/shared-dart/lib/src/theme/app_theme.dart (the shared Material 3 seed color). Edit the primary in these places and the whole system re-themes.

  3. 03

    Rename the apps and set your bundle IDs

    Display name — each app’s android/app/src/main/AndroidManifest.xml (android:label) and iOS ios/Runner/Info.plist (CFBundleDisplayName). Identifiers — android/app/build.gradle.kts (applicationId + namespace) and the iOS PRODUCT_BUNDLE_IDENTIFIER. The current names/IDs are in the App identity table below.
  4. 04

    Regenerate the launcher icons

    Run once inside each app folder (apps/customer, apps/rider, apps/restaurant) after swapping the masters in step 1:

    terminal
    dart run flutter_launcher_icons
  5. 05

    Point everything at your own backend and legal pages

    Three text edits, all outside the visual layer — do these before you ship a build:

    • Backend URL. The apps ship pointing at our demo backend. Set your own default in packages/shared-dart/lib/src/api/api_config.dart (ApiConfig.productionBaseUrl), or override per build with --dart-define=FEASTLY_API_URL=https://your-domain.
    • Terms & privacy links shown in every app’s Settings → About: _termsUrl / _privacyUrl in packages/shared-dart/lib/src/widgets/settings_about_section.dart. Point them at your own pages — the app stores require both.
    • Admin browser title. metadata.title / metadata.description in apps/web/src/app/layout.tsx (currently “Feastly Admin”) — this is the browser-tab name of your admin panel.
Note
That’s the whole rebrand. Logo (step 1) + one primary color (step 2) + names/IDs (step 3) + one command per app (step 4) + your URLs (step 5). No screen-by-screen editing — the design tokens and shared theme fan the change out across web and all three apps.
14

Verify the rebrand

Run through this once; if every row passes, no “Feastly” branding is left anywhere a user can see.

CheckWhere to lookExpected
App icon on the home screen
Install each of the three apps on a device/emulator
Your icon, not the Feastly mark
App name under the icon
Same — all three apps
Your names (step 3)
Brand color
Any primary button, the app bar, the “AI Spark” badge
Your primary, not #ae3200
Admin panel
Browser tab at /admin
Your title and favicon
Legal links
Settings → About in each app
Open your terms & privacy pages
Backend
Sign in on a release build with no --dart-define
Talks to your domain, not feastly.devsnack.dev
Store listings
applicationId / PRODUCT_BUNDLE_IDENTIFIER
Your reverse-domain IDs — these are permanent once published
Warning
Set bundle IDs before your first store upload. Android applicationId and the iOS bundle identifier can never be changed after an app is published — a new ID means a new listing that existing users won’t receive as an update. Everything else on this page can be changed at any time.
15

Design tokens, icons and app identity

Design tokens

The palette centers on primary orange #ae3200 (vibrant #ff5a1f) with a leaf-green accent #006d37, Inter throughout, 12px-radius buttons, 16px-radius cards, and the “AI Spark” gradient marker on AI surfaces. Web tokens live in the Tailwind preset (packages/config); mobile tokens live in packages/shared-dart.

Launcher icons

All three apps regenerate icons from shared masters in brand/ via flutter_launcher_icons:

terminal
# masters: brand/icon-1024.png (full), brand/foreground-1024.png (adaptive foreground)
# vector sources: brand/feastly-icon.svg, brand/feastly-mark.svg
dart run flutter_launcher_icons     # run inside apps/customer, apps/rider, apps/restaurant

To rebrand: replace the two 1024×1024 PNGs (optionally update the SVG masters), optionally change adaptive_icon_background in each app’s pubspec.yaml, then re-run the command. The web favicon is apps/web/src/app/favicon.ico.

App identity

AppDisplay nameAndroid applicationIdiOS bundle id
Customer
Feastly Customer
dev.devsnack.feastly_customer
dev.devsnack.feastlyCustomer
Rider
Feastly Rider
dev.devsnack.feastly_rider
dev.devsnack.feastlyRider
Restaurant
Feastly Restaurant
dev.devsnack.feastly_restaurant
dev.devsnack.feastlyRestaurant

Change the display name in each android/app/src/main/AndroidManifest.xml (android:label) and iOS Info.plist (CFBundleDisplayName); change ids in each android/app/build.gradle.kts (applicationId + namespace) and the iOS project’s PRODUCT_BUNDLE_IDENTIFIER. Min iOS is 17.0; Android inherits the Flutter default minSdk (raise it explicitly if your Flutter toolchain’s default is below what flutter_stripe requires). If you self-host, also update the production URL and the terms/privacy links in packages/shared-dart.

Note
i18n & currency. V1 ships a single language (an i18n scaffold is in place) and single-currency config (CURRENCY, ISO 4217, served to mobile via /api/mobile/config). Both are structured for later expansion; the expansion itself is not built.
16

FAQ and troubleshooting

Short answers to the questions new operators ask most — first the platform itself, then the deployment and day-two questions that come up once you go live. Full detail lives in Deployment, Database setup, Backups and exports, Optional integrations, Environment reference and Rebrand step by step.

Using the platform

Where do I sign in to the admin panel?
Open https://your-domain/login (locally http://localhost:3000/login) and sign in with an admin account. On the seeded demo that is admin@feastly.dev / password123. The three mobile apps each have their own login — a customer, rider or restaurant account can’t sign in to the admin panel, and vice-versa.
A page looks empty — did something break?
A brand-new database simply has no data yet. Run pnpm db:seed (Database setup) to load the demo — 12 restaurants, full menus, live and historical orders, and ledgers. In production the screens fill as real restaurants, orders and riders arrive. Every list also has a designed empty-state, so an empty screen is expected, not an error.
Where do I put my API keys (Stripe, R2, AI, Maps, SMTP)?
In the admin Configuration page (/admin/config) at runtime, or in the server .env at deploy time — the code reads env first, then the admin store. Keys are stored server-side only and are never sent to a mobile app. Leave any of them blank and that feature just stays off (Optional integrations). These are your own third-party accounts and may carry that provider’s own fees.
How do I approve a new restaurant or rider?
Admin → Restaurants (/admin/restaurants) or Riders (/admin/riders) → open the pending record → approve (or suspend). You can set a global commission % and override it per restaurant on the same screen.
How do I pay a restaurant or rider what they’re owed?
Admin → Payouts (/admin/payouts) lists amounts owed per restaurant and rider, computed from the immutable commission ledger. Settlement is manual in V1: pay them through your own bank/processor, then click mark as paid to record it.
The “AI” search looks like a normal search — where’s the AI Spark?
The AI layer is optional and stays off until you add an AI provider key (AI_PROVIDER + an OpenAI or Gemini key, AI setup). With it off, search runs as full-text and recommendations fall back to popularity — fully functional, just without the semantic ranking and the “AI Spark” badge.
The map shows a setup notice — what’s missing?
Google Maps is optional. Add your key (Environment reference): NEXT_PUBLIC_GOOGLE_MAPS_API_KEY for the admin geofence editor and MAPS_API_KEY (build-injected) for mobile. Without it the geofence editor shows a notice and nothing else breaks.
How do I change the language or currency?
V1 ships a single language (an i18n scaffold is in place for adding more later) and single-currency config. Set your market’s ISO-4217 code in CURRENCY; it’s served to the mobile apps via /api/mobile/config. See Design tokens, icons and app identity.
Do customers need the rider or restaurant app?
No. There are three separate apps, one per role: customers order in the Customer app, delivery drivers use the Rider app, and restaurant staff use the Restaurant app. You publish each to the stores under its own name and icon (Rebrand step by step).
How do I test a full order end-to-end?
On the seeded demo: order in the Customer app (customer@feastly.dev) → accept & mark ready in the Restaurant app (owner@feastly.dev) → go online, accept and deliver in the Rider app (rider@feastly.dev) → watch the Customer tracking screen advance live and the completed order surface in Admin → Orders and Payouts. Every demo account uses password123.

Deployment and operations

My Vercel deploy fails or builds the wrong project.
Nine times out of ten it’s one setting: Root Directory must be apps/web, not the repo root. This is a monorepo, so pointing Vercel at the root builds nothing usable. Set it under Project → Settings → General → Root Directory and redeploy. Framework detection, the build command and the install command are all automatic once that’s right (Deployment).
The site deployed, but every page errors with “relation … does not exist”.
The database has no schema yet. Vercel does not run migrations for you — run them once from your own machine against the production database:DATABASE_URL="<prod-url>" pnpm db:migrateDo not run pnpm db:seed against production; that’s demo data (Database setup).
Live tracking works locally but not after deploying to Vercel.
Expected, and documented. Realtime is an in-process pub/sub, so on serverless each request may hit a different instance and events don’t cross between them. Two fixes: set REDIS_URL and drop in the Redis adapter (a complete, copy-paste file is in Payments, media, push and realtime), or run apps/web on a persistent Node host/VPS where a single process stays alive. Order status is still correct either way — it just refreshes on fetch rather than streaming.
The mobile app can’t reach my backend.
A phone or emulator can’t see your computer’s localhost. Use the right host for the target: Android emulator http://10.0.2.2:3000, iOS simulator http://localhost:3000, real device your machine’s LAN IP or a public URL. Pass it at run/build time:flutter run --dart-define=FEASTLY_API_URL=http://10.0.2.2:3000Release builds need the flag too, or they use the default in ApiConfig.productionBaseUrl (Rebrand step by step, step 5). Note that iOS and Android both block plain http:// to public hosts — production must be https.
Sign-in works locally but fails on my production domain.
Check the three auth variables (Environment reference): AUTH_URL must equal your real domain, AUTH_TRUST_HOST=true is required behind a proxy or on Vercel, and AUTH_SECRET must be set and stable — changing it signs every existing session out. If you added a custom domain later, update AUTH_URL to match and redeploy.
I’m getting database connection errors.
Confirm DATABASE_URL is the pooled connection string from your provider and ends with ?sslmode=require — managed Postgres refuses plaintext connections. If your host has an IP allowlist, allow your deployment platform (Vercel’s egress IPs are dynamic, so a fully-restricted allowlist won’t work there). Any Postgres 15+ works; the docs use Neon’s free tier because it needs no allowlist.
Do I have to pay for any service to go live?
No. A working deployment needs a Postgres database and somewhere to run Node — both have free tiers. Cash on delivery is a first-class payment method, realtime is self-hosted, and mail is plain SMTP. Stripe, Cloudflare R2, Google Maps, FCM and the AI providers are all optional third-party accounts you connect yourself, each billed to you by that provider if you enable it (Optional integrations).
How do I update to a newer version of the item?
Download the new zip from your Envato Downloads page and diff it against your copy rather than overwriting wholesale — your .env, your brand/ masters and any files you customized should be kept. Then run pnpm install, pnpm db:migrate (new releases may add migrations) and rebuild the mobile apps. Back up first (Backups and exports) — a dump takes one command and makes any update reversible.
How do I back up before making a risky change?
One command: pg_dump "$DATABASE_URL" --format=custom --file=feastly-backup.dump. That single file is your full platform state and restores with pg_restore. Step-by-step instructions, CSV export and managed-host alternatives are in Backups and exports.
How do I publish the three apps to the stores?
Rebrand first (Rebrand step by step — icons, names and especially bundle IDs, which are permanent once published), then build each app with your own signing config and your backend URL:flutter build appbundle --dart-define=FEASTLY_API_URL=https://your-domainfor Google Play, flutter build ipa … for the App Store. Each app is submitted as its own listing. Both stores require reachable terms and privacy URLs — set them in step 5 of Rebrand step by step.
17

Production checklist

  1. 01
    Set DATABASE_URL and a stable AUTH_SECRET (openssl rand -base64 32) in the host environment.
  2. 02
    Set AUTH_URL to your production domain and AUTH_TRUST_HOST=true.
  3. 03
    Run pnpm db:migrate against the production database (do not seed it).
  4. 04
    Add only the optional keys you use — Stripe, R2, SMTP, Maps, AI — all server-side. Every variable and its blank-value behavior is in the complete environment reference.
  5. 05
    If enabling AI on a public deployment, add a rate limiter in front of /api/search and /api/recommendations (AI setup).
  6. 06
    For horizontally-scaled/serverless realtime, set REDIS_URL and back apps/web/src/lib/realtime.ts with Redis pub/sub — drop-in adapter in Payments, media, push and realtime.
  7. 07
    Schedule a backup: one pg_dump command as a nightly cron job or GitHub Action (Backups and exports).
  8. 08
    Build each mobile app with --dart-define=FEASTLY_API_URL=https://your-domain and your own signing config.
  9. 09
    Rebrand: swap brand/ masters, re-run flutter_launcher_icons, set app names/ids, update terms/privacy links.
  10. 10
    Set CURRENCY to your market’s ISO 4217 code.
18

Changelog

Every version published so far. Updates are free for the life of the item.

v1.1.0 · Aug 2026
Order details across all four apps.
v1.0.0 · Jul 2026
Initial release — four surfaces on a shared backend, order lifecycle state machine, commission and payout ledgers, COD and Stripe payments, self-hosted realtime tracking.

Version 1.1.0 · 2026-08-03

Order details across all four apps.

  • Rider order detail screen (new). Pickup and drop-off cards with one-tap Navigate and Call on each leg, the full item list with modifiers, delivery notes, and the cash-to-collect amount called out on COD orders.
  • Richer rider job cards. Before accepting, a rider sees the restaurant, its address, the delivery area with distance, item count, payment method and their payout — instead of just an order number and a total.
  • Restaurant order board + detail screen. Customer name, itemised order with modifiers, and the assigned rider with a Call button once one is allocated.
  • Admin order oversight. Expandable pickup / drop-off / assigned-rider contact block on every order row, so support can answer “where is this going, and to whom?”.
  • Customer tracking. Now shows which restaurant the order is coming from, with its address, Navigate and Call.
  • Customer privacy model. A customer’s street address and phone number are visible only to the rider actually assigned to their delivery, and to the platform admin. Riders browsing the unclaimed order pool see the delivery area and distance, never the customer’s identity or exact address. Enforced in the database queries, not just hidden in the UI.
  • Fixes. Delivery coordinates are now saved when a customer checks out with a saved address (previously the rider’s drop-off Navigate button had no destination); two riders can no longer accept the same order; pickup-only orders no longer appear in the rider delivery queue; and the restaurant’s address and phone now reach the customer’s tracking screen.
  • Demo data. Customer phone numbers, delivery coordinates, item modifiers, and a card-paid order so both the “collect cash” and “already paid” states are visible in the seed.
  • Upgrading. No database migration. Existing installations should run the one-off coordinate backfill below.
Note

Upgrading from 1.0.0 — one-off delivery-coordinate backfill. Only relevant if you already ran 1.0.0 in production and have real orders. In 1.0.0 an order’s delivery coordinates were saved only when the customer’s device sent an explicit GPS fix, so orders placed against a saved address stored none — and the rider app’s drop-off Navigate button had nothing to open. New orders are fixed in 1.1.0; this backfills the existing ones. It is a data update, not a migration — there is nothing to add to packages/db/drizzle, and fresh installations can skip it entirely.

sql
UPDATE orders o SET delivery_lat = a.lat, delivery_lng = a.lng
FROM addresses a
WHERE o.address_id = a.id AND o.delivery_lat IS NULL;

Safe to re-run — it only touches rows that still have a NULL delivery_lat, and never overwrites a coordinate that is already set.

Version 1.0.0 · 2026-07-10

  • Initial release. Four surfaces on a shared backend; order lifecycle state machine; commission & payout ledgers; COD + Stripe payments; self-hosted realtime tracking; optional AI search + recommendations; idempotent demo seed; full documentation.
19

Support and licensing

DevSnack — 6 months of item support is bundled per CodeCanyon standard (setup guidance, bug fixes, and answering usage questions). You supply your own hosting, domains and third-party accounts; we provide the walkthroughs.

  • Support requests: post on the Comments tab of this item’s CodeCanyon page — the fastest way to reach us.
  • Support tickets: open one from the Support tab on our CodeCanyon author profile.

License. Sold under the standard Envato Regular and Extended licenses. All dependencies are permissive (MIT / Apache-2.0 / BSD / ISC); no copyleft or per-seat obligation is passed to you.

What support covers

  • Six months of support from purchase, extendable at checkout
  • Support covers bugs in the template and questions about how it is put together
  • It does not cover custom feature work, third-party API changes or store review outcomes
  • The Regular licence covers one free end product. Charging users for the app itself needs the Extended licence