Crately documentation
Run it, point it at your own accounts, ship it under your own brand. Written against version 1.0.0 of the download you get on CodeCanyon.
- Version v1.0.0
- Updated —
- Platform Flutter
- Stack Flutter · Postgres
On this page
Overview
AI-powered B2B food supplier SaaS — three apps, one source. Customer Flutter app · Driver Flutter app · Next.js admin + web + API.
A complete operating system for a wholesale food supplier serving 10–500 restaurant customers.
Crately is a 3-in-1 bundle: a Next.js 16 codebase (admin panel + customer web + marketing + REST API), a customer Flutter app, and a driver Flutter app. The positioning is deliberately single-supplier-many-customers (one food wholesaler, many restaurant accounts) — not a multi-vendor marketplace.
Every B2B-specific concept is wired: account-level pricing, credit terms (NET-15/30/COD/prepaid), delivery cutoff times, standing orders, multi-location restaurant chains, owner-approval workflow, quality credits applied to invoices, FIFO/FEFO inventory batches. Eight AI features ship enabled out of the box, talking to OpenAI or Google Gemini through a server-only wrapper so the keys never leave your infrastructure.
Architecture is serverless-first: Vercel functions + a managed Postgres (Neon free tier handles thousands of orders/day) + Inngest cron. No Kubernetes, no Docker, no separate worker hosts.
Live demo: https://crately.devsnack.dev — sign in with admin@demo.com / demopassword.
What’s included
- Full source for all three apps (
crately_web/,crately_app/,crately_driver/). - Drizzle ORM schema covering 40+ tables, with migrations 0000–0004 ready to apply on a fresh Postgres.
- Industrial Harvest design system — Tailwind v4 config + tokens, Flutter
ThemeData, brand color tokens, typography (Sora + JetBrains Mono). - Email templates — 7 React Email templates (order confirmation, invoice issued, quality credit, password reset, abandoned cart, pre-cutoff, standing-order pre-submit).
- PDF templates — invoice + statement via React-PDF.
- Branding pipeline — single SVG source under
branding/→ one command generates favicon, PWA icons, Flutter launcher icons for iOS + Android (color + inverted variants). - Documentation — this guide,
README.md(overview + setup),DEPLOY.md(production), plus inline comments on every non-obvious helper. - 6 months of Item Support (per the Envato Item Support Policy) · free future updates.
Features
Customer Flutter app (crately_app/)
- JWT login (HS256, 30-day TTL) against
POST /api/v1/auth/login - Catalog browse — categories, product grid, product detail with account-scoped pricing, search
- Cart with multi-line management; multi-location switcher for chain restaurants
- Checkout with delivery-date picker, customer notes, owner-approval gate above threshold
- Orders list (Active / Past) with status pills; detail with items + status timeline
- Realtime delivery tracking — Google Map with driver pin, polls every 8s, Pusher private channel for sub-second updates
- Invoices list + detail + PDF download (token-signed URL for external browser)
- Stripe Pay Online on open invoices with deep-link return (
crately://) - AI Order Predictor — one-tap to populate the cart from history
- AI Recipe → Order — chef pastes a recipe, AI returns priced ingredients
- Standing orders — create / view / edit / pause / resume; per-line notes
- Profile + sign-out · FCM push notifications · brand mark in login + header
Driver Flutter app (crately_driver/)
- JWT login (driver role enforced)
- Today’s route — stops grouped, sorted by AI route optimization when available
- Job detail with line items; start-delivery transition
- Deliver flow — photo capture (camera), customer signature pad, optional receiver name
- AI Quality Audit auto-fires on delivery upload — flagged audits surface in
/admin/creditsfor one-click credit - Vehicle check — 6-item pre-route checklist with fuel + odometer + notes
- Earnings — week / month / all-time, daily breakdown
- Issues reporting — 7 categories (customer not available, wrong address, damaged goods, vehicle breakdown, etc.), optional order linkage
- GPS streaming every 15s while on shift (paired with the customer-side tracker)
Admin panel (crately_web/admin)
- Real KPI dashboard — today’s orders + revenue, 7-day chart, AR aging buckets, AI alerts (replenishment / credit risk / demand insights), top accounts this month, recent activity feed
- Catalog — categories + products CRUD, AI Description Generator on product create
- Customer accounts — list, onboarding queue, account detail, pricing tiers
- Orders — list with filters, detail with status transitions, driver assignment, picking, AI substitution
- Inventory — stock, expiry, wastage
- Invoices + AR aging + payments + quality credits (with AI-flagged audit review)
- Suppliers + purchase orders — auto-receive triggers inventory batch on “Mark received”
- Sales reps + drivers + routes + delivery zones
- Promotions · Communications (template editor) · Pages (marketing CMS) · Reports (sales, top products, AR aging — all with CSV export) · Settings (business profile, branding, integrations status)
- AI screens — Insights (Sunday digest), Smart Replenishment (nightly plan), Credit Risk (daily refresh), Demand Insights weekly
Backend — Next.js API routes + Inngest crons
REST API surface lives under /api/v1/. Both web (cookie session) and mobile (JWT) auth paths are supported through the requireCustomer / requireDriver helpers in src/lib/api/.
13 Inngest functions handle cron + event-triggered work:
Architecture
crately/
crately_web/ Next.js 16 + Drizzle + JWT auth + Inngest
crately_app/ Flutter customer app (Riverpod 3 + GoRouter + Dio)
crately_driver/ Flutter driver app (same stack)
branding/ SVG logo source of truth + raster pipeline
docs/ Buyer documentation + this CodeCanyon bundleTech stack
src/lib/ai/client.tsFolder convention
Every app uses a feature-first layout. Backend: src/app/api/v1/<feature>/ + src/server/services/<feature>.ts. Flutter: lib/features/<feature>/. AI calls live exclusively under src/lib/ai/ and src/app/api/v1/ai/.
Quick setup
Follow the steps below to get running locally. For production deploy guidance see DEPLOY.md at the repo root.
- 01Extract zip,
cd crately_web && pnpm install - 02Create
crately_web/.env.localwithDATABASE_URL,AUTH_SECRET,NEXT_PUBLIC_APP_URL - 03
pnpm db:migrate && pnpm dev→http://localhost:3000 - 04Register your first owner at
/register, then use the admin panel to create products, accounts, pricing tiers, and delivery zones. - 05
In separate terminals, run each Flutter app with:
terminalflutter run --dart-define=API_BASE=http://localhost:3000
Database
Any Postgres works. Neon free tier handles a small wholesaler comfortably. Pick a region near your Vercel function region — by default vercel.json pins sin1 (Singapore); if you change either, change both.
# Provision Postgres (Neon)
# Copy the connection string, then:
echo 'DATABASE_URL=postgres://...' >> crately_web/.env.local
echo 'AUTH_SECRET='"$(openssl rand -base64 32)" >> crately_web/.env.local
echo 'NEXT_PUBLIC_APP_URL=http://localhost:3000' >> crately_web/.env.local
cd crately_web
pnpm db:migrate # applies 0000–0004The database starts empty after migration. Register your first admin user at /register, then create products, accounts, pricing tiers, and delivery zones via the admin panel. The hosted demo at crately.devsnack.dev/admin (login admin@demo.com / demopassword) shows what a populated deployment looks like.
Useful Drizzle commands:
pnpm db:studio— Drizzle GUIpnpm db:generate— generate a new migration after schema editspnpm db:push— push schema directly (dev only, skips migration files)
Optional integrations
Every integration gracefully no-ops when its env vars are missing. The codebase ships a has*() probe pattern — see src/lib/{stripe,fcm,pusher,email}.ts — so you can run a fully working demo without provisioning any third-party services.
STRIPE_SECRET_KEY, STRIPE_WEBHOOK_SECRETPUSHER_APP_ID, PUSHER_KEY, PUSHER_SECRET, PUSHER_CLUSTER, plus the NEXT_PUBLIC_* mirror pairFIREBASE_SERVICE_ACCOUNT_JSON (the whole JSON as one var)RESEND_API_KEY, RESEND_FROM_ADDRESSTWILIO_ACCOUNT_SID, TWILIO_AUTH_TOKEN, TWILIO_FROM_NUMBERUPLOADTHING_TOKEN (falls back to public/uploads/)UPSTASH_REDIS_REST_URL, UPSTASH_REDIS_REST_TOKEN (falls back to in-memory)GOOGLE_MAPS_API_KEY, NEXT_PUBLIC_GOOGLE_MAPS_API_KEY + iOS xcconfig + Android local.propertiessrc/lib/stripe.ts — lazy init, has*() probe, warn-on-failure in calling code.AI providers
Two server-side providers, dual-keyed. Pick whichever fits your budget — bring at least one key.
# crately_web/.env.local
OPENAI_API_KEY=sk-...
# OR
GEMINI_API_KEY=AIza...
AI_DEFAULT_PROVIDER=openai # or geminiPer-day rate limits per account, defaults in src/lib/env.ts:
src/lib/ai/client.ts. The Flutter apps only ever talk to /api/v1/ai/* — they never see your OpenAI key.Payments (Stripe)
- 01Create a Stripe account at stripe.com, grab
STRIPE_SECRET_KEY. - 02
Stripe Dashboard → Developers → Webhooks → Add endpoint:
- URL:
https://<your-domain>/api/webhooks/stripe - Events:
checkout.session.completed
Reveal the signing secret → copy as
STRIPE_WEBHOOK_SECRET. - URL:
- 03
Local dev:
terminalstripe listen --forward-to localhost:3000/api/webhooks/stripeThe CLI prints the dev signing secret to use in
.env.local. - 04On the customer Flutter app, the “Pay $X online” button opens Checkout in the system browser and returns into the app via the
crately://invoices/{id}?paid=1deep link. The order detail provider auto-invalidates so thePAIDstatus appears within a second of Stripe’s webhook firing.
PAYPAL_CLIENT_ID + PAYPAL_CLIENT_SECRET are reserved in .env.example but the PayPal flow is not yet implemented. Roadmap.Push notifications (FCM)
Push triggers fire from src/lib/push/order-notifications.ts on order status transitions + Stripe payment success.
- 01Create a Firebase project, enable Cloud Messaging.
- 02Project Settings → Service accounts → Generate new private key. Paste the entire JSON into one env var:
FIREBASE_SERVICE_ACCOUNT_JSON. - 03
In
crately_app/:dart pub global activate flutterfire_cli, thenflutterfire configure --project=<your-id>. This overwrites the committed placeholders atandroid/app/google-services.jsonandios/Runner/GoogleService-Info.plistwith your real values. Then guard those two files from accidental commits:terminalgit update-index --skip-worktree android/app/google-services.json git update-index --skip-worktree ios/Runner/GoogleService-Info.plist - 04iOS only: Xcode → Runner target → Signing & Capabilities → add Push Notifications + Background Modes (Remote notifications).
- 05Same as step 3 in
crately_driver/if you want push for drivers.
Notification taps deep-link back into the app via the existing crately:// URL scheme — same plumbing as Stripe return.
google-services.json and GoogleService-Info.plist with REPLACE_WITH_PROJECT_ID markers — that’s why a fresh clone builds on both Android and iOS without you having configured Firebase yet. The Gradle hasFirebase guard reads the file and skips the Google Services plugin when the marker is present. iOS keeps the file as a bundle resource so Xcode’s “Copy Bundle Resources” step succeeds; at runtime Firebase.initializeApp() throws and main.dart catches it. Push features simply no-op until you complete this section.Realtime delivery tracking (Pusher)
- 01At dashboard.pusher.com/channels/apps create a Channels app (not Beams). Pick a cluster close to your Vercel region —
ap1for Singapore. - 02
Copy keys into the env:
crately_web/.env.local# Server-side PUSHER_APP_ID=... PUSHER_KEY=... PUSHER_SECRET=... PUSHER_CLUSTER=ap1 # Client-side (Flutter reads these via /api/v1/config) NEXT_PUBLIC_PUSHER_KEY=... NEXT_PUBLIC_PUSHER_CLUSTER=ap1 - 03The driver app streams location every 15s while signed in (
features/tracking/location_reporter.dart). On every fix the server triggersprivate-order-{id}events. The customer app subscribes viafeatures/tracking/pusher_subscriber.dartwhen viewing an order with statusOUT_FOR_DELIVERY. - 04If you don’t configure Pusher, the customer app falls back to polling every 8s — still works, just less snappy.
Google Maps
Required for the realtime tracking map. Enable these APIs in Google Cloud Console:
- Maps SDK for iOS
- Maps SDK for Android
- Maps JavaScript API
Use one key per platform with appropriate restrictions:
crately_app/ios/Flutter/Secrets.xcconfig (copied from the .example; gitignored)crately_app/android/local.properties (already gitignored)GOOGLE_MAPS_API_KEY + NEXT_PUBLIC_GOOGLE_MAPS_API_KEY in .env.localRestrict the iOS key to your bundle id com.devsnack.crately.restaurant; restrict the Android key to your package + signing SHA-1; restrict the web key to your domain.
Branding and customization
Single SVG source under branding/. One command regenerates every platform’s expected size.
# Edit branding/mark-color.svg (or mark-inverted.svg for the driver variant)
cd crately_web
pnpm icons:build # writes web favicon + PWA + Flutter master PNGs
cd ../crately_app && dart run flutter_launcher_icons
cd ../crately_driver && dart run flutter_launcher_iconsTheme tokens
- Web:
crately_web/src/app/globals.css(CSS custom properties) +tailwind.config.ts - Flutter customer:
crately_app/lib/core/theme/{theme,colors}.dart - Flutter driver:
crately_driver/lib/core/theme/{theme,colors}.dart
Brand color today: #0f5238 (Deep Forest Green). Surface: #f8faf6 (Sage Cream). The Material 3 token set is called “Industrial Harvest” — the full palette lives in crately_web/src/app/globals.css (CSS variables) and the matching Flutter colors.dart files.
App name and bundle id
Customer app uses com.devsnack.crately.restaurant, driver uses com.devsnack.crately.driver. Files to edit when you rebrand:
pubspec.yamlin each Flutter app (thename:line + theflutter_launcher_iconsblock)ios/Runner/Info.plist—CFBundleDisplayNameandroid/app/build.gradle.kts—applicationId+namespaceandroid/app/src/main/AndroidManifest.xml—android:label- iOS bundle id via Xcode Signing & Capabilities
Marketing copy
The landing page lives in crately_web/src/app/(marketing)/page.tsx as a single static file. Hero, features grid, pricing tiers, FAQ — all editable in one place. Two TODO markers indicate where to drop in a real screenshot of your branded admin/customer screen and where to swap in your CodeCanyon listing URL.
i18n
next-intl is wired but EN-only today. The messages/ folder is ready for AR (RTL), ID, MS, ES, FR — extract strings into messages/<locale>.json and wrap user-facing text in useTranslations(). Multi-language support is on the roadmap.
Signing and store distribution
How to produce a signed Android AAB for the Play Console and an archived iOS build for the App Store. Do this for the customer app and again for the driver app — they have separate bundle IDs and need separate listings.
Android — generate the upload keystore
One-time setup per app. Lose this keystore and you can’t update your app on the Play Store ever again — back it up to a password manager or secure cloud storage.
# From any directory:
keytool -genkey -v \
-keystore ~/keystore/crately-customer-release.jks \
-keyalg RSA -keysize 2048 -validity 10000 \
-alias crately
# Repeat with a different filename + alias for the driver app:
keytool -genkey -v \
-keystore ~/keystore/crately-driver-release.jks \
-keyalg RSA -keysize 2048 -validity 10000 \
-alias crately-driverThe tool prompts for a store password, key password, your name, organisation, etc. Remember the two passwords — you’ll paste them into the next step.
Android — wire the keystore into Gradle
The Gradle signing config already ships in both apps — build.gradle.kts reads a key.properties file and applies it to the release build automatically. You only need to create that file; no Gradle edits required. Create crately_app/android/key.properties (gitignored — never commit):
storePassword=<the store password you just set>
keyPassword=<the key password you just set>
keyAlias=crately
storeFile=/Users/<you>/keystore/crately-customer-release.jksThat’s it — the next flutter build --release detects key.properties and signs with your keystore. (When the file is absent, the release build falls back to the debug keystore so a fresh clone still builds locally; ship-ready release builds require the file.) Repeat for the driver app: create crately_driver/android/key.properties pointing at crately-driver-release.jks with keyAlias=crately-driver.
Android — build and upload
cd crately_app
flutter build appbundle --release
# Output: build/app/outputs/bundle/release/app-release.aab
# For the driver app:
cd ../crately_driver
flutter build appbundle --release- 01Play Console → Create app → set package name to match your
applicationId(com.devsnack.crately.restaurantfor customer,com.devsnack.crately.driverfor driver — or your rebranded equivalents). - 02Production → Create new release → upload the
.aab. - 03Fill the Store listing (icon 512×512 + screenshots + description). Use the brand mark from
branding/regenerated at 512×512 for the Play icon. - 04Roll out to Internal testing first → Closed testing → Production.
Android — get the SHA-1 for Firebase + Google Maps
When you upload an AAB to Play Console, Google re-signs it with their own key. Both your upload keystore and the Play Store signing certificate need to be registered with Firebase and the Google Maps key restrictions.
# Upload key SHA-1
keytool -list -v \
-keystore ~/keystore/crately-customer-release.jks \
-alias crately
# Play Store app-signing SHA-1: copy from
# Play Console → Setup → App integrity → App signing key certificateAdd both SHA-1 values:
- Firebase Console → Project settings → Your apps → Android app → Add fingerprint.
- Google Cloud Console → APIs & Services → Credentials → your Android Maps key → Application restrictions → add the package name + SHA-1.
~/keystore/*.jks + the matching key.properties into a password manager. Losing either means Play Store rejects every future update.iOS — Apple Developer prerequisites
- Apple Developer Program membership ($99/yr) at developer.apple.com.
- Xcode 15+ on a Mac.
- For push: an APNs key (create at Apple Developer → Certificates, Identifiers & Profiles → Keys) — upload it in Firebase Console → Project settings → Cloud Messaging → APNs Authentication Key.
iOS — bundle id and capabilities
- 01Open
crately_app/ios/Runner.xcworkspacein Xcode. - 02Select the Runner target → Signing & Capabilities.
- 03Check Automatically manage signing and select your Apple Developer Team. Xcode will auto-create the App ID, provisioning profile, and signing certificate.
- 04Set Bundle Identifier to whatever you want shipped on the App Store (default
com.devsnack.crately.restaurant; rebrand tocom.<your-org>.<app>). - 05
Click + Capability and add:
- Push Notifications
- Background Modes → tick Remote notifications
- 06Repeat for the driver app in
crately_driver/ios/Runner.xcworkspacewith its own bundle ID (e.g.com.devsnack.crately.driver).
iOS — App Store Connect app record
- 01Go to App Store Connect → My Apps → + → New App.
- 02Fill: platform iOS, name “Crately” (or your rebrand), primary language, bundle ID (must match step 4 above), SKU (any unique string).
- 03Repeat for the driver app.
iOS — build, archive, upload
cd crately_app
flutter build ipa --release
# Output: build/ios/archive/Runner.xcarchive
# Open the archive:
open build/ios/archive/Runner.xcarchiveXcode Organizer opens. Click Distribute App → App Store Connect → Upload. Wait for processing (~5–30 min) — you’ll get an email when it’s available in TestFlight.
Then in App Store Connect:
- 01TestFlight → invite internal testers → confirm the build runs on a real device.
- 02App Store → fill app metadata (description, keywords, screenshots — 6.7" iPhone screenshots are mandatory).
- 03Submit for review. Apple typically responds in 24–48 hours.
Repeat the build / upload / TestFlight / review for the driver app in crately_driver/.
iOS — common rejection reasons (preempt these)
- Guideline 5.1.1 (data & privacy) — your
NSCameraUsageDescription+NSLocationWhenInUseUsageDescriptionstrings inInfo.plistmust explain the user-facing reason (“To capture proof-of-delivery photos”, “To show your driver on the live tracking map”). These are already populated in the bundle — review the wording for your brand. - Guideline 3.1.1 — Apple wants in-app purchases to use StoreKit, not Stripe, for digital goods consumed inside the app. Crately’s Stripe checkout pays for physical food delivery which is explicitly exempt (Guideline 3.1.5 — physical goods sold from a B2B platform). Mention this in the App Review Notes field if asked.
- Sign-in test account — provide
owner@demo.com/demopasswordagainst your deployed backend in the App Review Information section. Without working demo creds, reviewers can’t get past the login screen and reject the app.
Production checklist
- 01Branding — replace
branding/mark-color.svg+ runpnpm icons:buildthendart run flutter_launcher_iconsin each Flutter dir. - 02Theme tokens — edit
crately_web/src/app/globals.css+ both Fluttercolors.dartfiles. - 03App names + bundle IDs in the Flutter pubspecs + native config (see Branding and customization).
- 04Provision Postgres — Neon recommended; match your Vercel region.
- 05Install + migrate —
pnpm installthenpnpm db:migrate. Register your first admin at/registerand build your catalog via the admin panel. - 06Env vars — set
DATABASE_URL,AUTH_SECRET,NEXT_PUBLIC_APP_URL+ any optional integrations. - 07Vercel — import the GitHub repo, set
crately_web/as the root directory, set region to match Postgres, paste env vars, deploy. The build command (pnpm db:migrate && pnpm build) is preset incrately_web/vercel.json. - 08Stripe webhook — add the endpoint, paste signing secret into Vercel env.
- 09Pusher Channels — create the app, paste all six env vars into Vercel.
- 10Firebase — create project → enable Cloud Messaging → generate service account JSON → paste into
FIREBASE_SERVICE_ACCOUNT_JSON. - 11Flutter apps —
flutterfire configure --project=<id>in each. iOS Xcode: enable Push Notifications + Background Modes. - 12
Build Flutter releases:
- iOS:
flutter build ipa --release→ archive in Xcode → upload via App Store Connect. - Android:
flutter build appbundle --release→ upload to Play Console.
- iOS:
- 13Inngest Cloud (optional, free tier covers a small wholesaler) — install the Inngest GitHub app, sign with
INNGEST_SIGNING_KEY, the functions auto-register on deploy. - 14UploadThing (optional) — production storage for driver delivery photos.
- 15Promote yourself to admin — find your user row, set
role = 'ADMIN'or'SUPER_ADMIN'. - 16Privacy + Terms — admin panel → Pages → Seed defaults → edit copy → publish. Update the marketing footer link.
Changelog
Every version published so far. Updates are free for the life of the item.
Version 1.0.0
Initial release. Three apps; 8 AI features; Stripe + Pusher + FCM + Resend wired; Industrial Harvest design system; brand mark + launcher icons; admin dashboard with real data; static marketing landing; deploy guide.
Support and licensing
Bundled 6 months of email support per CodeCanyon standard — covers setup help, bug clarifications, and pointer questions. Custom development is hourly.
Hosting (Vercel), database (Neon / your choice), third-party accounts (Stripe, Pusher, Firebase, Resend, etc.) are the buyer’s responsibility. The bundle is source code, not a managed SaaS.
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