Storify Setup & Installation
Everything you need to install, configure, and deploy Storify - from your first pnpm install to AI, payments, messaging, marketplace billing, POS, and production hosting.
Overview
Storify is a production-ready AI-powered ecommerce platform built with Next.js 16, React 19, Tailwind CSS 4, MongoDB, and Better Auth. This documentation covers setup, configuration, payments, AI, omnichannel messaging, marketplace billing, POS, localization, deployment, and ongoing maintenance for release 1.3.1.
Read sectionQuickstart
If you already have Node, pnpm, and MongoDB ready, this is the fastest path to a working Storify installation. Each step is expanded later in the docs.
Read sectionRequirements
Storify runs anywhere modern Node.js apps can run. These versions match the bundled package and are the supported baseline for buyer installs.
Read sectionDownload & Setup
Download from CodeCanyon, unzip the package, and install dependencies. Storify ships as a ready-to-run Next.js project.
Read sectionStorify is a production-ready AI-powered ecommerce platform built with Next.js 16, React 19, Tailwind CSS 4, MongoDB, and Better Auth. This documentation covers setup, configuration, payments, AI, omnichannel messaging, marketplace billing, POS, localization, deployment, and ongoing maintenance for release 1.3.1.
What's in the package
Full source code, storefront, admin dashboard, vendor dashboard, staff and POS workflows, database models, seed and migration scripts, operating docs under /docs, and PWA assets.
Commerce modes
Run a single-vendor store or enable marketplace mode with a vendor registration wizard, approval, commissions, payouts, vendor dashboards, and optional paid vendor subscription plans.
AI Studio & Sales Agent
A storefront AI Sales Agent plus a dashboard AI Studio that writes product, category, collection, brand, and blog copy, SEO metadata, and generates or edits images.
Omnichannel inbox
Storefront live chat unified with WhatsApp, Facebook Messenger, Instagram Direct, and Telegram in one conversation model with assignment, templates, and escalation.
Payments
Stripe, PayPal, Razorpay, Paystack, Pesapal, ioTec, and cash-on-delivery are built into checkout and admin settings.
POS, inventory & barcodes
A responsive POS register with held orders and receipt printing, multi-location inventory, transfers, pre-orders, EAN/UPC/GTIN barcodes, and thermal label printing.
Physical & digital products
Variants with visual swatches, global variant templates, 3D models, external video embeds, size guides, and digital products with private downloads and free samples.
Storage choices
Local disk, Cloudflare R2, or any S3-compatible bucket, with a provider-aware Media Library and automatic WebP conversion for uploaded images.
Recommended first path
Start with Quickstart, then Environment Variables, then Database Setup and Admin & Roles. Once you can log in, work through Settings Reference from the admin dashboard.
Upgrading from an older release
If you already run 1.0, 1.1, 1.2, or 1.3, read Updates & Migrations before deploying. Several releases ship index and data migrations that must run once against your existing database.
If you already have Node, pnpm, and MongoDB ready, this is the fastest path to a working Storify installation. Each step is expanded later in the docs.
- 1
Install dependencies
From the unzipped Storify source folder, install all packages with pnpm.
bashcd storify pnpm install - 2
Create the env file
Copy the bundled .env.example to .env, then set MONGODB_URI and generate BETTER_AUTH_SECRET. Payment, SMTP, OAuth, analytics, storage, and messaging credentials can also be saved later from Admin -> Settings, which always wins over .env.
bashcp .env.example .env openssl rand -base64 32 # paste into BETTER_AUTH_SECRET - 3
Connect MongoDB and seed
With MONGODB_URI set, load demo store data so you can explore admin, vendor, customer, products, orders, POS, inbox, and settings immediately.
bashpnpm db:seed - 4
Start the dev server
Open http://localhost:3000. The seed command prints demo credentials in your terminal. Change all demo passwords before production.
bashpnpm dev
BETTER_AUTH_SECRET is mandatory
A production build refuses to start while the secret is empty, a known placeholder, or shorter than 32 characters. Generate a real value before pnpm build.
Storify runs anywhere modern Node.js apps can run. These versions match the bundled package and are the supported baseline for buyer installs.
Node.js 20.19.28+
Use Node 20 LTS or newer. The app uses Next.js 16.2, React 19.2, and the React Compiler, so Node 18 is not supported.
pnpm 10.24.0
The package pins pnpm in packageManager and engines. Enable it with corepack, then run pnpm install from the project root.
MongoDB 6+ (7 recommended)
Mongoose 9 and the MongoDB 7 driver ship in the package. Atlas is recommended; self-hosted works when MONGODB_URI and MONGODB_DB_NAME are correct.
Storage (choose one)
Local disk for a single persistent server, or Cloudflare R2 / any S3-compatible bucket for product images, 3D models, blog and category media, chat attachments, and digital files.
A scheduler for cron routes
Email delivery retries, vendor subscription billing, the messaging outbox, and escalations run as authenticated HTTP GET routes. Vercel Cron is preconfigured; on a VPS use system cron.
Optional third-party services
OpenAI for AI features, a Meta developer app for WhatsApp/Messenger/Instagram, a Telegram bot, QZ Tray for direct thermal printing, and an SMTP provider for transactional email.
Hosting suggestion
For most buyers, Vercel + MongoDB Atlas + Cloudflare R2 is the fastest path. VPS and Docker are covered below, and are the better choice if you want the Local storage provider.
Local storage needs a persistent disk
The Local provider writes under the app's upload directory. It suits a single VPS or container with a mounted volume, not serverless or multi-instance deployments where the filesystem is ephemeral.
Download from CodeCanyon, unzip the package, and install dependencies. Storify ships as a ready-to-run Next.js project.
- 1
Download from CodeCanyon
Sign in to CodeCanyon -> Downloads -> Storify. Choose 'All files & documentation'.
- 2
Unzip the package
Extract the archive to a permanent location. The main application folder is usually /storify, and the bundled operating guides live in /storify/docs.
bashunzip storify-v1.3.1.zip -d ~/projects/ cd ~/projects/storify - 3
Install dependencies
Storify uses pnpm for fast, deterministic installs. Run this once after every download or update.
bashcorepack enable pnpm install - 4
Verify the install
Run lint, types, tests, and a production build before deployment. All four are the smoke check the package is shipped against.
bashpnpm lint pnpm typecheck pnpm test pnpm build
Use pnpm only
The lockfile is pnpm-only. Mixing package managers can corrupt dependency resolutions.
Storify scripts read from .env, and Next.js also loads it during development and production builds. Integration credentials can live here or in Admin -> Settings; a value saved in Settings always wins, and the matching variable below acts as a per-field fallback. Keep this file private and never commit real secrets.
# ---- Database (required) ----
MONGODB_URI=mongodb://localhost:27017/storify?authSource=admin
MONGODB_DB_NAME=storify
# MONGODB_MAX_POOL_SIZE=50
# RATE_LIMIT_STORE=memory
# ---- Better Auth (required) ----
BETTER_AUTH_SECRET=
BETTER_AUTH_URL=http://localhost:3000
# ---- App configuration (required) ----
NEXT_PUBLIC_APP_URL=http://localhost:3000
NEXT_PUBLIC_APP_NAME=Storify
NEXT_PUBLIC_SUPPORT_EMAIL=support@example.com
DEMO_MODE=false
NEXT_PUBLIC_ENABLE_PWA_IN_DEV=false
# ---- Initial admin fallbacks for pnpm create-admin ----
ADMIN_NAME=Admin
ADMIN_PASSWORD=change-me
# ---- OpenAI (optional, billed by OpenAI) ----
OPENAI_API_KEY=your-openai-api-key-here
AI_HERO_BANNER_ENABLED=false
# ---- Scheduled jobs ----
CRON_SECRET=replace-with-a-long-random-secret
# ---- Email / SMTP ----
SMTP_HOST=smtp.example.com
SMTP_PORT=587
SMTP_USER=your-smtp-username
SMTP_PASS=your-smtp-password
SMTP_FROM="Storify <no-reply@example.com>"
# ---- Web push (optional) ----
WEB_PUSH_PUBLIC_KEY=your-web-push-public-key-here
WEB_PUSH_PRIVATE_KEY=your-web-push-private-key-here
WEB_PUSH_SUBJECT=mailto:admin@example.com
NEXT_PUBLIC_WEB_PUSH_PUBLIC_KEY=your-web-push-public-key-here
# ---- Omnichannel messaging (optional) ----
MESSAGING_ENCRYPTION_KEY=replace-with-at-least-32-random-characters
META_APP_ID=your-meta-app-id
META_APP_SECRET=your-meta-app-secret
META_WHATSAPP_CONFIGURATION_ID=your-login-for-business-configuration-id
META_INSTAGRAM_CONFIGURATION_ID=your-instagram-login-configuration-id
META_WEBHOOK_VERIFY_TOKEN=replace-with-a-long-random-verify-token
META_GRAPH_API_VERSION=vXX.X
# ---- OAuth / social login (optional) ----
GOOGLE_CLIENT_ID=your-google-client-id
GOOGLE_CLIENT_SECRET=your-google-client-secret
FACEBOOK_APP_ID=your-facebook-app-id
FACEBOOK_APP_SECRET=your-facebook-app-secret
# ---- Payment gateways (optional) ----
NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY=pk_test_xxx
STRIPE_SECRET_KEY=sk_test_xxx
STRIPE_WEBHOOK_SECRET=whsec_xxx
PAYPAL_CLIENT_ID=your-paypal-client-id
PAYPAL_CLIENT_SECRET=your-paypal-client-secret
PAYPAL_WEBHOOK_ID=your-paypal-webhook-id
NEXT_PUBLIC_PAYSTACK_PUBLIC_KEY=pk_test_xxx
PAYSTACK_SECRET_KEY=sk_test_xxx
NEXT_PUBLIC_RAZORPAY_KEY_ID=rzp_test_xxx
RAZORPAY_KEY_SECRET=your-razorpay-key-secret
RAZORPAY_WEBHOOK_SECRET=your-razorpay-webhook-secret
PESAPAL_MODE=sandbox
PESAPAL_CONSUMER_KEY=your-pesapal-consumer-key
PESAPAL_CONSUMER_SECRET=your-pesapal-consumer-secret
PESAPAL_IPN_ID=your-registered-pesapal-ipn-id
IOTEC_MODE=sandbox
IOTEC_CLIENT_ID=your-iotec-client-id
IOTEC_CLIENT_SECRET=your-iotec-client-secret
IOTEC_WALLET_ID=your-iotec-wallet-uuid
# ---- Object storage (optional) ----
STORAGE_ACCESS_KEY_ID=your-storage-access-key-id
STORAGE_SECRET_ACCESS_KEY=your-storage-secret-access-key
STORAGE_ACCOUNT_ID=your-r2-account-id
STORAGE_ENDPOINT=
STORAGE_REGION=auto
STORAGE_BUCKET=your-bucket-name
STORAGE_PUBLIC_URL=https://your-public-bucket-url.com
# ---- Analytics & thermal printing (optional) ----
NEXT_PUBLIC_GA_ID=G-XXXXXXXXXX
NEXT_PUBLIC_GTM_ID=GTM-XXXXXXX
NEXT_PUBLIC_FACEBOOK_PIXEL_ID=your-facebook-pixel-id
NEXT_PUBLIC_TIKTOK_PIXEL_ID=your-tiktok-pixel-id
PLAUSIBLE_API_KEY=your-plausible-api-key
QZ_TRAY_CERTIFICATE=""
QZ_TRAY_PRIVATE_KEY=""| Key | Required | Description |
|---|---|---|
MONGODB_URI | Required | MongoDB connection string for the Storify database. Use the local example for development or an Atlas URI in production. |
MONGODB_DB_NAME | Required | Database name used by app scripts, migrations, and admin creation commands. |
MONGODB_MAX_POOL_SIZE | Optional | Connections one app instance may hold open. Defaults to 50. Keep instances x pool size under your Mongo plan's connection limit. |
RATE_LIMIT_STORE | Optional | Where API rate-limit counters live. Unset keeps them in MongoDB so limits hold across instances. Set to memory only for a single instance. |
BETTER_AUTH_SECRET | Required | Random 32+ character secret used by Better Auth to sign sessions. A production build refuses to start on an empty, short, or placeholder value. |
BETTER_AUTH_URL | Required | Canonical app URL used by Better Auth for callbacks and trusted origins. |
NEXT_PUBLIC_APP_URL | Required | Public storefront URL used in links, metadata, emails, gateway callbacks, and client-side configuration. Pesapal IPN registration requires a public HTTPS value. |
NEXT_PUBLIC_APP_NAME | Optional | Fallback store name used in transactional emails and notifications until a store name is saved in Admin -> Settings -> General. It does not change the browser tab title or page metadata. |
NEXT_PUBLIC_SUPPORT_EMAIL | Optional | Support address shown on the access-denied page. Order emails and invoices use the store email from Admin -> Settings -> General instead. |
DEMO_MODE | Optional | Set to true on public demos. Visitors can still create products, edit orders, and upload images; deletes, settings edits, and test actions are refused per route. |
NEXT_PUBLIC_ENABLE_PWA_IN_DEV | Optional | Enables the PWA service worker during local development. Leave false unless you are testing installability. |
ADMIN_NAME | Optional | Fallback display name used by pnpm create-admin when the name argument is omitted. |
ADMIN_PASSWORD | Optional | Fallback password used by pnpm create-admin when the password argument is omitted. Change it before any real deployment. |
OPENAI_API_KEY | Optional | Powers the AI Sales Agent and AI Studio. Without it, AI features stay unavailable. Usage is billed by OpenAI. Can also be saved in Admin -> Settings -> AI Configuration. |
AI_HERO_BANNER_ENABLED | Optional | Opt-in switch for the admin home page hero banner generator. Defaults to disabled. |
CRON_SECRET | Optional | Shared secret for the scheduled routes. Required if you use email delivery retries, vendor subscription billing, the messaging outbox, or escalations. |
SMTP_HOST | Optional | SMTP server hostname. Required for password resets, staff invites, and order email unless configured in Admin -> Settings -> Email. |
SMTP_PORT | Optional | SMTP port, usually 587 for STARTTLS or 465 for implicit TLS. |
SMTP_USER | Optional | SMTP username for your email provider. |
SMTP_PASS | Optional | SMTP password or API key. Server-only; never expose it to the browser. |
SMTP_FROM | Optional | Default From header, for example "Storify <no-reply@example.com>". The domain should have SPF, DKIM, and DMARC configured. |
WEB_PUSH_PUBLIC_KEY | Optional | Public VAPID key for browser push subscriptions. Generate with pnpm push:keys. |
WEB_PUSH_PRIVATE_KEY | Optional | Private VAPID key used by the server to sign push payloads. Keep it secret. |
WEB_PUSH_SUBJECT | Optional | VAPID subject, usually a mailto: address or your production app URL. |
NEXT_PUBLIC_WEB_PUSH_PUBLIC_KEY | Optional | The same public VAPID key, exposed to the browser so the service worker can subscribe. |
MESSAGING_ENCRYPTION_KEY | Optional | 32+ character key that encrypts stored channel tokens for WhatsApp, Messenger, Instagram, and Telegram. Never rotate it without re-encrypting existing connections. |
META_APP_ID | Optional | Meta developer app id used by WhatsApp embedded signup and Messenger connections. |
META_APP_SECRET | Optional | Meta app secret used to verify every inbound webhook signature before the payload is parsed. |
META_WHATSAPP_CONFIGURATION_ID | Optional | Facebook Login for Business configuration id for WhatsApp embedded signup. Without it, the manual WABA/phone/token form is the fallback. |
META_INSTAGRAM_CONFIGURATION_ID | Optional | Optional second configuration granting Instagram messaging permissions. Without it, the Instagram panel falls back to the manual Page-token form. |
META_WEBHOOK_VERIFY_TOKEN | Optional | Long random value you also enter as the verify token in the Meta app webhook setup. |
META_GRAPH_API_VERSION | Optional | Graph API version pinned for your Meta app, for example v21.0. |
GOOGLE_CLIENT_ID / GOOGLE_CLIENT_SECRET | Optional | Google OAuth credentials. Credentials alone do not enable the provider — the Admin -> Settings toggle decides whether it is active. |
FACEBOOK_APP_ID / FACEBOOK_APP_SECRET | Optional | Facebook OAuth credentials, also gated behind the admin panel toggle. |
STRIPE_SECRET_KEY / STRIPE_WEBHOOK_SECRET | Optional | Stripe server credentials for checkout payments and vendor subscription billing. NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY is the browser-side fallback. |
PAYPAL_CLIENT_ID / PAYPAL_CLIENT_SECRET / PAYPAL_WEBHOOK_ID | Optional | PayPal credentials and webhook id used for order capture and verification. |
PAYSTACK_SECRET_KEY | Optional | Paystack secret key. NEXT_PUBLIC_PAYSTACK_PUBLIC_KEY is used by the browser checkout. |
RAZORPAY_KEY_SECRET / RAZORPAY_WEBHOOK_SECRET | Optional | Razorpay server credentials. NEXT_PUBLIC_RAZORPAY_KEY_ID is used by the browser checkout. |
PESAPAL_MODE / PESAPAL_CONSUMER_KEY / PESAPAL_CONSUMER_SECRET / PESAPAL_IPN_ID | Optional | Pesapal API 3.0 credentials, sandbox or live mode, and the IPN id returned when you register the notification URL from admin settings. |
IOTEC_MODE / IOTEC_CLIENT_ID / IOTEC_CLIENT_SECRET / IOTEC_WALLET_ID | Optional | ioTec Pay client-credentials and wallet id for Ugandan mobile money and card collections. |
STORAGE_ACCESS_KEY_ID / STORAGE_SECRET_ACCESS_KEY | Optional | S3-compatible credentials for Cloudflare R2 or AWS S3. Not needed when the Local storage provider is selected. |
STORAGE_ENDPOINT / STORAGE_REGION / STORAGE_BUCKET / STORAGE_ACCOUNT_ID | Optional | Bucket coordinates. For R2 set the endpoint to https://<account-id>.r2.cloudflarestorage.com and the region to auto; for S3 set the region and leave the endpoint blank. |
STORAGE_PUBLIC_URL | Optional | Public base URL or CDN domain used to serve uploaded files. CLOUDFLARE_R2_PUBLIC_URL is still honoured as a legacy fallback. |
NEXT_PUBLIC_GA_ID / NEXT_PUBLIC_GTM_ID / NEXT_PUBLIC_FACEBOOK_PIXEL_ID / NEXT_PUBLIC_TIKTOK_PIXEL_ID | Optional | Client-side analytics and pixel ids. Also configurable from Admin -> Settings -> Analytics. |
PLAUSIBLE_API_KEY | Optional | Server-side Plausible key used by the admin analytics dashboard. |
QZ_TRAY_CERTIFICATE / QZ_TRAY_PRIVATE_KEY | Optional | Signed QZ Tray certificate and key for direct thermal printing. The private key is read only by the server signing route and must never be public. |
Generate BETTER_AUTH_SECRET fast
Run openssl rand -base64 32 in your terminal and paste the output as the value. Use the same command for CRON_SECRET, META_WEBHOOK_VERIFY_TOKEN, and MESSAGING_ENCRYPTION_KEY.
Two sources of configuration
Payments, OAuth, SMTP, storage, analytics, and AI credentials can be set here or in Admin -> Settings. The database value always wins, so you can boot from .env and later override individual fields from the UI. Messaging is the exception: MESSAGING_ENCRYPTION_KEY and the META_* variables are read from .env only, and the messaging settings page manages per-channel connections rather than these credentials.
Never rotate MESSAGING_ENCRYPTION_KEY in place
Channel tokens are encrypted with it. Rotating the key without re-encrypting stored connections makes every WhatsApp, Messenger, Instagram, and Telegram connection unreadable.
Storify stores users, vendors, products, categories, orders, settings, payments, reviews, blog posts, inventory, conversations, subscriptions, and audit history in MongoDB. A fresh install creates its indexes automatically; existing databases upgrade with the migration scripts below.
- 1
Create a MongoDB database
Create a MongoDB Atlas cluster and database named storify, or prepare a self-hosted MongoDB instance.
bash# MongoDB Atlas example MONGODB_URI=mongodb+srv://username:password@cluster0.mongodb.net/storify MONGODB_DB_NAME=storify - 2
Allow network access
In MongoDB Atlas, add your local IP address for development and your hosting provider's outbound IPs for production.
txtAtlas -> Network Access -> Add IP Address - 3
Seed demo data
Loads demo settings, admin, vendors, customers, staff, categories, products, orders, coupons, collections, inventory locations, and sample store data.
bashpnpm db:seed - 4
Reset only for testing
Use reset only on a disposable database. It clears existing records before loading demo ecommerce data. db:full-reset does both in one command.
bashpnpm db:reset # or, reset and reseed in one go pnpm db:full-reset - 5
Run migrations on an existing database
Every migration has a :dry twin that reports what it would change without writing. Run the dry pass first, read the output, then run the real command. New installs can skip all of them.
bashpnpm db:migrate:indexes:dry && pnpm db:migrate:indexes
pnpm db:migrate:indexes # product & review indexes
pnpm db:migrate:perf-indexes # general performance indexes
pnpm db:migrate:dashboard # dashboard aggregation indexes
pnpm db:migrate:vendor-storefront-indexes # vendor storefront queries
pnpm db:migrate:account-status # one-time account status upgrade
pnpm db:migrate:omnichannel # messaging collections & indexes
pnpm db:migrate:telegram-keys # scope Telegram message ids per chat
pnpm db:migrate:support-conversations # legacy support tickets -> conversations
pnpm db:migrate:push # push subscription records
pnpm db:migrate:audit-indexes # audit retention (90 days -> 24 months)
pnpm db:migrate:barcodes # unique barcode registry
pnpm db:migrate:vendor-billing # vendor subscription billing backfillProduction: skip the seed
Never run pnpm db:seed, pnpm db:reset, or pnpm db:full-reset against a live store database. They insert demo records and accounts intended for evaluation.
Always dry-run first
pnpm db:migrate:barcodes:dry reports duplicate product or variant barcodes you must resolve before global uniqueness can be enforced. The Telegram migration reports colliding keys instead of guessing which message to keep.
Automatic index creation
Fresh installations build their indexes on boot unless MONGODB_AUTO_INDEX=false. On large production databases, disable it and create indexes during a maintenance window with the migration scripts.
Use dev for local work and build/start for production validation. The storefront, admin, vendor, staff, POS, and API routes all run from the same Next.js app.
- 1
Start in development
Hot reload, fast refresh, and source maps. Defaults to port 3000.
bashpnpm dev - 2
Run a production build
Compiles, optimizes, and runs the Next.js production server. Use this before deploying changes.
bashpnpm build pnpm start - 3
Check types and tests while you work
Typecheck and the Vitest suite run independently of the dev server, and cover AI authoring, barcodes, stock policy, variants, storage, shipping labels, and the home page config.
bashpnpm typecheck pnpm test - 4
Create a real admin
For production, create your own admin account instead of relying on seed credentials. Password and name fall back to ADMIN_PASSWORD and ADMIN_NAME when omitted.
bashpnpm create-admin admin@yourdomain.com "StrongPassword123!" "Store Admin"
Change demo passwords
If you use seeded data for a staging preview, change all seeded passwords before exposing the URL to anyone else.
Running a public demo
Set DEMO_MODE=true. Visitors can still create products, edit orders, and upload images, while deletes, settings changes, and connection tests are refused per route so the seeded store survives.
Use this as the checklist for Admin -> Settings. It maps all twenty settings pages to the configuration each one controls, so you know exactly where every store behavior is managed.
General
Store name, description, email, phone, domain, and address, timezone, default language and currency, supported languages and currencies, and the store-wide country availability policy. All groups sit on one page as cards.
Branding
Logo, dark logo, and favicon, plus primary, secondary, and accent colors applied across the whole application, preset palettes including your own, theme mode with light as the default, contrast, RTL, collapsed sidebar, and navigation color.
Multi-Vendor Management
Enables marketplace mode and vendor permissions for products, orders, store settings, analytics, payouts, and POS access. Links through to Vendor Configuration for registration, approval, trials, required documents, and the default commission.
POS
POS availability for admins, vendors, and sellers, default POS location, receipt and printing behavior, smart grid, sounds, offline payments, payment methods, return rules, and the opt-in switch for selling vendor-owned stock on the register.
Inventory Locations
Creates active and default inventory locations used by admin stock, vendor stock, transfers, POS inventory, held orders, and fulfillment workflows.
Two-Factor Authentication
Opt-in 2FA for the store, and can require it for admins and vendors. Disabled by default so a fresh install is not locked behind an authenticator app.
OAuth / Social Login
Google and Facebook login with client IDs, app IDs, and saved secrets. The toggle here decides whether a provider is active — credentials present in .env never auto-enable it.
AI Configuration
Shared OpenAI key, text and image model selection, output size and quality, per-surface switches, staff and vendor access, daily usage limits, brand voice, tone, image style, brand kit colors and logo, and a connection test.
Security & Access Control
Email verification, vendor verification, session duration, login attempts, lockout duration, password policy, and rate-limit presets for admin, vendor, checkout, cart, coupon, and auth routes.
Payment Settings
Stripe, PayPal, Razorpay, Paystack, Pesapal, ioTec, and cash-on-delivery — public keys, saved secrets, webhook secrets, sandbox or live mode, Pesapal IPN registration, order status sync, and per-gateway connection tests.
Email Configuration (SMTP)
SMTP enablement, host, port, username, saved password, from email and name, SSL/TLS, email and vendor verification rules, delivery logs, and test email sending.
Notification Settings
Which events raise dashboard, email, browser push, and native device push notifications for admins, vendors, staff, and customers.
Omnichannel Messaging
Storefront live chat with its availability mode, weekly schedule, widget color, and offline message; the escalation toggle, interval, and email; and channel connections for WhatsApp, Messenger, Instagram, and Telegram with the webhook callback URL.
Order Settings
Order number prefix, tax rate, default shipping cost, free shipping threshold, vendor commission rate, and minimum withdrawal amount.
Shipping & Delivery
Shipping origin, processing days, estimated delivery display, shipping zones, flat, free-over, and subtotal-range rates, fallback rates, and local pickup.
SEO Settings
Meta title, meta description, meta keywords, and Open Graph image for storefront SEO. robots.txt is generated by the app itself and is not editable here.
Social / Links
Facebook, X/Twitter, Instagram, YouTube, LinkedIn, and TikTok URLs for the storefront footer and public metadata.
Analytics
Google Analytics 4, Google Tag Manager, Meta Pixel, TikTok Pixel, and Plausible, including a self-hosted Plausible domain and API URL.
Maintenance
A temporary maintenance screen, custom message, and optional allowed IPs so admins keep access during an update.
Storage
Selects Local, Cloudflare R2, or S3-compatible storage; stores account, endpoint, region, bucket, and access credentials, public CDN URL, upload path prefix, allowed MIME types, size limits, and opens the Media Library.
/en/admin/settings/general
/en/admin/settings/appearance # labelled "Branding" in the sidebar
/en/admin/settings/marketplace
/en/admin/settings/pos
/en/admin/settings/locations
/en/admin/settings/two-factor
/en/admin/settings/oauth
/en/admin/settings/ai
/en/admin/settings/security
/en/admin/settings/payment
/en/admin/settings/email
/en/admin/settings/notifications
/en/admin/settings/messaging
/en/admin/settings/orders
/en/admin/settings/shipping
/en/admin/settings/seo
/en/admin/settings/social
/en/admin/settings/analytics
/en/admin/settings/maintenance
/en/admin/settings/storage
# Marketplace surfaces that live outside Settings
/en/admin/vendors/configuration # registration, approval, trials, commission
/en/admin/vendors/onboarding # required documents + registration wizard
/en/admin/vendors/plans # subscription plan catalogSettings vs environment variables
Most store behavior is controlled from Admin -> Settings after deployment. Environment variables are still required for app bootstrapping, Better Auth, the database connection, cron authentication, and server-only fallback secrets.
Recommended launch order
Configure General, Storage, Payment, Email, Shipping, SEO, Analytics, Security, then AI, Messaging, POS, and Multi-Vendor. Test checkout, upload, email, chat, and login before going live.
Storify stores product images, 3D models, category thumbnails, blog images, chat attachments, and digital deliverables through one storage service. Pick Local disk, Cloudflare R2, or any S3-compatible provider from Admin -> Settings -> Storage.
- 1
Choose a provider
Local writes to the server's own disk and suits a single persistent VPS or container with a mounted volume. Cloudflare R2 and S3-compatible buckets suit serverless and multi-instance deployments. You can switch later; existing media keeps resolving through its original provider.
- 2
Create a bucket (R2 or S3 only)
Create a Cloudflare R2 or AWS S3 bucket for Storify uploads. Use a lowercase bucket name with numbers and hyphens only.
txtstorify-media - 3
Create scoped access credentials
For R2, create an API token with Object Read & Write permission scoped to the bucket. For S3, create an IAM user or access key with least-privilege object access. Custom endpoints, path-style addressing, and region normalization are all supported.
- 4
Connect a public media URL
For production, connect a custom domain such as assets.yourdomain.com. For local testing, an R2 public development URL works. The Local provider serves files from the app itself and needs no public URL.
txthttps://assets.yourdomain.com - 5
Set limits and verify
Set the upload path prefix, allowed MIME types, and max image, video, and 3D model sizes. Save the draft configuration, run the built-in provider test, then upload a product image and a 3D model from the product editor.
- 6
Browse everything in the Media Library
Admin -> Settings -> Storage opens a provider-aware Media Library with pagination, search, media-type filtering, upload, image, video, 3D, and document previews, URL copying, and single or bulk deletion.
https://<ACCOUNT_ID>.r2.cloudflarestorage.com # region: autoKeep write credentials server-only
Never expose storage access keys in NEXT_PUBLIC_* variables or client-side code. Only public media URLs should be browser-readable.
Image handling
Uploaded and reusable images are converted to WebP, and media dimensions are stored so the storefront can reserve layout space. Attachments sent on external channels — WhatsApp, Messenger, Instagram, Telegram — are the deliberate exception and keep their original format, because those providers reject WebP. Images in the in-app live chat are still converted.
Private files are separate
Digital product deliverables go to a private, scoped key prefix and are never referenced by a storefront payload. Customers reach them only through short-lived signed links from a paid order. See Products & Downloads.
Storify supports Stripe, PayPal, Razorpay, Paystack, Pesapal, ioTec Pay, and cash-on-delivery. Enable only the gateways you want from Admin -> Settings -> Payment Settings; every credential there overrides its .env fallback.
- 1
Add Stripe keys
Enable Stripe, then enter the publishable key, secret key, and webhook secret. Saved secrets are hidden after save. Stripe also powers vendor subscription billing, so a marketplace with paid plans must configure it.
Stripe fieldstxtPublishable Key Secret Key Webhook Secret Test connection - 2
Configure webhooks and callbacks
Point each gateway at its deployed endpoint so orders are marked paid after gateway confirmation. All of these must be publicly reachable over HTTPS and must not sit behind authentication.
bashhttps://yourdomain.com/api/payments/webhook # Stripe (orders + vendor billing) https://yourdomain.com/api/payments/razorpay/webhook # Razorpay https://yourdomain.com/api/payments/paystack/webhook # Paystack https://yourdomain.com/api/payments/pesapal/ipn # Pesapal IPN (GET + POST) https://yourdomain.com/api/payments/iotec/callback # ioTec Pay - 3
Subscribe the Stripe events
Order payments need the payment intent events. If you sell vendor subscription plans, subscribe the billing events to the same endpoint — without them, a paid checkout never activates the plan.
Vendor billing eventstxtcheckout.session.completed checkout.session.expired customer.subscription.created customer.subscription.updated customer.subscription.deleted invoice.paid invoice.payment_failed invoice.payment_action_required - 4
Add the other gateways
Use sandbox or test keys first, then test each enabled gateway before switching to live mode. Pesapal and ioTec both cache their access tokens for the advertised lifetime, so checkout stays a single round trip.
Gateway fieldstxtPayPal: Client ID, Client Secret, Mode, Webhook ID Razorpay: Key ID, Key Secret, Webhook Secret Paystack: Public Key, Secret Key Pesapal: Consumer Key, Consumer Secret, Mode, IPN ID ioTec: Client ID, Client Secret, Wallet ID, Mode COD: Instructions, minimum amount, maximum amount - 5
Register the Pesapal IPN
Save the Pesapal consumer key, secret, and mode, set NEXT_PUBLIC_APP_URL to a public HTTPS domain (use a tunnel for sandbox development), then choose Register IPN in payment settings. Storify registers the IPN URL and stores the returned id.
- 6
Switch to live mode
Replace test keys with live keys, create production webhooks in the same mode as the keys, make a small real payment, then verify the order, transaction, and refund records.
Cash-on-delivery
COD does not require gateway keys. Enable it from payment settings if your store supports offline collection, and set the minimum and maximum order amounts it applies to.
Regional gateways
Pesapal covers East African cards and mobile money through hosted checkout. ioTec Pay covers Ugandan mobile money and cards, and adds Ugandan shilling support to the currency list.
Test and live modes must match
A webhook signed with a test secret will not validate against live keys. When a payment succeeds but the order stays pending, this mismatch is the first thing to check.
Password resets, staff invites, order emails, abandoned checkout recovery, vendor billing notices, and customer notifications use the SMTP transport. Configure it in Admin -> Settings -> Email Configuration, or through the SMTP_* variables as a fallback.
- 1
Save and test the transport
Enter host, port, username, password, from email, and from name, then send a test email from the same page. Delivery logs on that page show what was attempted and what failed.
- 2
Schedule the delivery retry job
Failed sends are queued rather than dropped. Schedule the delivery route every five minutes with the CRON_SECRET bearer token so retries actually run — Vercel's bundled cron config already does this.
bashcurl -H "Authorization: Bearer $CRON_SECRET" \ https://yourdomain.com/api/cron/email-deliveries - 3
Choose which events send mail
Admin -> Settings -> Notification Settings decides which events reach admins, vendors, staff, and customers by email, dashboard notification, browser push, and native device push.
Resend
Simple for new stores. In Admin -> Settings -> Email Configuration, use smtp.resend.com as host and resend as username.
SendGrid
Good for higher send volume and mature sender reputation. Enter the SendGrid SMTP host, port, username, and API password in Email Configuration.
Postmark
Excellent for transactional email such as receipts, password reset, and order updates.
Generic SMTP
Works for testing and smaller shops. Use production-grade credentials before launch.
Verify your sender domain
Always set up SPF, DKIM, and DMARC for the From Email domain you configure in Admin -> Settings -> Email Configuration. Without domain authentication, store emails often land in spam.
Storify has two AI surfaces: a storefront AI Sales Agent that answers product, payment, and delivery questions, and a dashboard AI Studio that writes copy and generates images for products, variants, categories, collections, brands, blogs, hero slides, promotional cards, and review replies.
- 1
Add an OpenAI API key
Save the key in Admin -> Settings -> AI Configuration, or set OPENAI_API_KEY in .env as a fallback and restart. The settings page runs a connection test and reports whether OpenAI is reachable.
bashOPENAI_API_KEY=your-openai-api-key-here - 2
Pick models and output settings
Choose a text model (GPT-4.1 mini, GPT-4.1, GPT-5 mini, or GPT-5) and an image model (GPT Image 1 or GPT Image 1 mini), then set output size and quality. Cheaper models are the sensible default for bulk product copy.
- 3
Turn on the surfaces you want
Each AI surface has its own switch, so you can enable product descriptions without enabling blog authoring. Staff and vendor access are separate toggles, and daily usage limits cap spend per account.
- 4
Set brand voice and brand kit
Configure tone, brand instructions, image-style guidance, primary and secondary brand colors, and a logo. Generated copy and artwork follow them across every surface.
- 5
Enable the hero banner studio (optional)
The home page hero generator produces exact 1360 x 314 artwork and is opt-in because it consumes image credits quickly.
bashAI_HERO_BANNER_ENABLED=true - 6
Configure the Sales Agent
Open Admin -> AI Sales Agent to adjust prompt behavior, store context, suggested questions, and availability on the storefront.
Keep AI keys private
OPENAI_API_KEY must stay server-side. Never add it as NEXT_PUBLIC_OPENAI_API_KEY. AI routes enforce role and surface gates, rate limits, and daily usage accounting on the server.
Usage is billed by OpenAI
Every generation costs tokens or image credits at OpenAI's pricing. Set daily limits before giving staff or vendors access, and review usage in AI settings.
Deeper reference
The bundled /docs/AI_AUTHORING_OPERATING_LAYER.md documents every AI surface, its prompt contract, and its permission rules.
Storefront live chat, WhatsApp, Facebook Messenger, Instagram Direct, and Telegram share one conversation model and one inbox for admins, vendors, and staff. Live chat works with no external setup; the social channels need a Meta app or a Telegram bot.
- 1
Set the messaging secrets
MESSAGING_ENCRYPTION_KEY encrypts every stored channel token, and CRON_SECRET authenticates the outbox and escalation jobs. Set both before connecting any channel.
bashMESSAGING_ENCRYPTION_KEY=at-least-32-random-characters CRON_SECRET=a-long-random-value - 2
Turn on live chat
Admin -> Settings -> Omnichannel Messaging enables storefront live chat and sets its availability mode, weekly schedule, widget color, offline message, and the escalation toggle, interval, and email. This alone needs no third-party account. The public contact form always creates a conversation in the same inbox — there is no toggle, and it works even with live chat switched off.
- 3
Create and point a Meta app
Configure a Meta developer app with WhatsApp and/or Messenger, then use the callback shown in the settings page as the webhook URL and META_WEBHOOK_VERIFY_TOKEN as the verify token. Subscribe the WhatsApp message and status events and the Messenger Page messaging events.
bashhttps://yourdomain.com/api/webhooks/meta - 4
Connect WhatsApp
With META_WHATSAPP_CONFIGURATION_ID set, owners connect through Facebook Login for Business (embedded signup); Storify verifies the selected phone belongs to the returned WABA and subscribes the app. The manual WABA, phone, and token form remains as a fallback.
- 5
Connect Messenger and Instagram
Messenger uses a Page token from the channel panel. Instagram Direct runs on the same Messenger Platform — set META_INSTAGRAM_CONFIGURATION_ID for the guided flow, otherwise use the manual Page-token form.
- 6
Connect Telegram
Create a bot with BotFather and paste its token into the Telegram channel panel. Storify registers the webhook for you.
bashhttps://yourdomain.com/api/webhooks/telegram - 7
Schedule the outbox and escalations
Outbound sends retry through the outbox route and unanswered conversations escalate through its own route. Both authenticate with CRON_SECRET; vercel.json already schedules them at one and five minutes.
bashGET /api/cron/messaging-outbox # every minute GET /api/cron/messaging-escalations # every 5 minutes - 8
Run the messaging migrations
Existing installations create the conversation collections and indexes with the omnichannel migration. Stores that used the old support tickets or connected Telegram before message ids were scoped per chat run the other two.
bashpnpm db:migrate:omnichannel:dry && pnpm db:migrate:omnichannel pnpm db:migrate:support-conversations pnpm db:migrate:telegram-keys
The Meta webhook must be public
It has to be reachable over HTTPS and must not sit behind authentication. Every POST is signature-checked with META_APP_SECRET before the JSON is parsed, so an unsigned request is rejected anyway.
Account steps Meta owns
App review, business verification, permissions, and phone registration happen in your Meta account and cannot be completed from source code. WhatsApp templates need whatsapp_business_management; sending and receiving also needs whatsapp_business_messaging.
Deeper reference
The bundled /docs/OMNICHANNEL_MESSAGING.md covers channel capabilities, Meta and Instagram setup, template synchronization, vendor controls, and scaling the SSE stream beyond the default MongoDB poll.
Multi-vendor mode adds vendor registration, approval, commissions, payouts, and vendor dashboards. On top of that, an optional subscription system lets you charge vendors for plans through Stripe, with per-plan commission rates and limits.
- 1
Enable multi-vendor mode
Admin -> Settings -> Multi-Vendor Management turns marketplace mode on and sets vendor permissions for products, orders, store settings, analytics, payouts, and POS access. In single-vendor mode Storify uses the default main store vendor.
- 2
Configure vendor registration
Admin -> Vendors -> Configuration controls whether registration is open, whether approval is manual, the plan toggle, plan trial length, and the default commission.
txt/en/admin/vendors/configuration - 3
Shape the onboarding wizard
Admin -> Vendors -> Onboarding owns the required verification documents and the registration wizard itself — rename and reorder steps, toggle required and hidden fields, add custom fields, and preview the result live.
txt/en/admin/vendors/onboarding - 4
Build the plan catalog (optional)
Admin -> Vendors -> Plans defines each plan's price, billing interval, commission rate, trial length, product and staff limits, and whether AI authoring is included. Plans stay entirely optional — leave the system off to run a commission-only marketplace.
txt/en/admin/vendors/plans - 5
Wire Stripe billing
Vendor plans synchronize to Stripe products and prices, then use Checkout and the customer portal for upgrades, downgrades, and cancellations. Configure Stripe in the same mode as your plans and subscribe the billing events listed in Payment Gateways.
- 6
Schedule the billing job
An hourly authenticated GET drives renewals, dunning retries before expiry, and expiry handling. Without it, a failed payment never retries and an expired plan never reverts its commission.
bashcurl -H "Authorization: Bearer $CRON_SECRET" \ https://yourdomain.com/api/cron/vendor-subscriptions - 7
Backfill existing vendors
Stores upgrading from a release before vendor billing run the backfill once so existing vendors get consistent subscription and commission records.
bashpnpm db:migrate:vendor-billing:dry && pnpm db:migrate:vendor-billing
One commission authority
Commission resolves from the store, the vendor's plan, and the settings default in that order, and the result is projected onto the vendor record. Plan expiry reverts commission lazily at read time without revoking the vendor's access.
Deeper reference
The bundled /docs/vendor-subscription-stripe-checklist.md is the operational runbook: required deployment configuration, recovery when Stripe took payment but the local record is incomplete, dunning, plan changes, and diagnostics.
The POS register works on phones, tablets, and desktops for admins, vendors, and staff. It shares stock with the rest of the store through inventory locations, and drives barcode labels, receipts, and shipping labels.
- 1
Create inventory locations
Admin -> Settings -> Inventory Locations defines the active and default locations used by admin stock, vendor stock, transfers, POS inventory, held orders, and fulfillment. A register must have a location before it can sell counted stock.
- 2
Configure POS access
Admin -> Settings -> POS controls availability for admins, vendors, and sellers, the default location, receipt behavior, smart grid, sounds, offline payments, accepted payment methods, and return rules.
- 3
Decide on vendor stock
Selling vendor-owned stock on the shop's register is opt-in: it needs pos.allowVendorProducts, multi-vendor mode, and a POS location, because without a per-store count there is nothing proving the goods are on the shelf.
- 4
Prepare the barcode registry
Barcodes are globally unique across products and variants. Run the dry pass first, resolve every duplicate it reports, then apply the migration.
bashpnpm db:migrate:barcodes:dry # resolve duplicates, then pnpm db:migrate:barcodes - 5
Assign identifiers
Enter a barcode with its format and source, or use Generate for an internal check-digit-valid EAN-13. Auto-detection covers EAN-13, UPC-A, GTIN-14, and Code 128.
- 6
Print barcode labels
Select rows in Inventory and open Barcode labels: 40 x 25, 50 x 30, or 60 x 40 mm, 203 or 300 DPI, ZPL or TSPL, manual copies or on-hand quantity. Print through the browser, download the raw commands, or send directly to a QZ printer.
- 7
Set up direct thermal printing (optional)
Install QZ Tray on each terminal with the printer's normal driver, then add a signed certificate and key on the server. Restart the app after changing them; the private key is only ever read by the server-side signing route.
bashQZ_TRAY_CERTIFICATE="-----BEGIN CERTIFICATE-----\n...\n-----END CERTIFICATE-----" QZ_TRAY_PRIVATE_KEY="-----BEGIN PRIVATE KEY-----\n...\n-----END PRIVATE KEY-----"
Held orders are per register
A parked sale belongs to its POS location, and resuming one re-reads every line against the live catalogue so stale prices and stock another register already sold are caught before checkout.
Browser printing checklist
Browser output uses exact millimetre page sizes. Disable browser headers and footers, print at 100% or Actual size, and calibrate the printer's media and gap sensor before a production run.
Deeper reference
The bundled /docs/BARCODE_AND_THERMAL_PRINTING.md covers supported identifiers, first deployment, the label studio, QZ setup, POS receipts, and 4 x 6 shipping labels.
Every product is either physical or digital, and that choice decides which sections of the form apply. Physical products carry weight, customs data, and counted stock; digital products carry private download files and never run out.
Format is chosen once
The physical or digital switch is locked after creation in both the form and the API. The two formats own different data, checkout paths, and stock semantics, and existing carts and orders reference the old shape. Create a new product to change format.
Stock policy
Digital products, products with Track quantity off, and products set to continue selling when out of stock are never blocked by the stock count. The same rule drives the buy box, cart, product cards, and inventory writes.
Digital deliverables
Files upload to a private, scoped storage prefix — up to 20 per product, 5 GB each — and are never referenced by a storefront payload. Buyers download them from a paid order through short-lived signed links.
Free samples
A digital product can also carry one public sample file, linked from the product page before purchase, the way a book preview works.
Variants and options
Options support text, color, image, integer, and decimal values with swatch or image presentation. Global variants and category-level templates merge into product variants while preserving product-owned options.
Rich media
Product media accepts images, 3D models, and embedded YouTube or Vimeo video, with a size guide covering product-line templates, regional sizing, and measurement instructions.
URL handles
A product's URL handle follows its title until you edit it, then stays fixed so a published link keeps working. Clear the field to resume automatic generation.
Digital files need durable storage
With the Local provider, digital deliverables live on the app server's disk. Make sure that path is on a mounted volume and included in your backups, or a redeploy will take the files with it.
Storify ships as an installable PWA with browser web push and native device push delivery, so order updates, messages, and stock alerts reach staff and customers outside the tab.
- 1
Generate push keys
Run the bundled command and copy the public and private pair into .env.
bashpnpm push:keys - 2
Set web push variables
Paste the generated VAPID values. The public key appears twice — the server signs with the private key, and the browser subscribes with the NEXT_PUBLIC_ copy.
bashWEB_PUSH_PUBLIC_KEY=your-web-push-public-key-here WEB_PUSH_PRIVATE_KEY=your-web-push-private-key-here WEB_PUSH_SUBJECT=mailto:admin@example.com NEXT_PUBLIC_WEB_PUSH_PUBLIC_KEY=your-web-push-public-key-here - 3
Migrate existing subscriptions
Stores upgrading from a release before native device push run the push migration once.
bashpnpm db:migrate:push:dry && pnpm db:migrate:push - 4
Choose what gets pushed
Admin -> Settings -> Notification Settings maps each event to in-app, email, and push per role. The single Push toggle covers browser web push and native device delivery together.
- 5
Set your app icon
The favicon in Admin -> Settings -> Branding is what the web manifest and install prompt use, so upload it there and reinstall the PWA to see the new icon. With nothing configured the manifest declares no icon and the platform falls back to its own.
- 6
Test installability
Build and serve over HTTPS, then install from Chrome or Edge. To exercise the service worker locally, set NEXT_PUBLIC_ENABLE_PWA_IN_DEV=true.
No bundled placeholder icons
Since 1.3.1 the package ships no default favicon or PWA icon set. With nothing configured, sidebars and the SEO preview fall back to a store-initial badge instead of advertising the platform.
Storify includes admin-managed store settings plus code-level brand defaults. Use admin settings for day-to-day changes and source files for packaged defaults.
- 1
Update general store settings
Open Admin -> Settings -> General to set store name, contact details, address, timezone, default and supported languages and currencies, and the country availability policy. All groups sit on one page as cards with a shared save bar.
- 2
Upload logos and the favicon
Admin -> Settings -> Branding holds the logo, dark logo, and favicon. They are served from settings, so a fresh install shows your mark in the browser tab, the web manifest, the service worker, sidebars, and the SEO preview. The favicon doubles as the PWA install icon — there is no separate app-icon upload.
- 3
Set the color system
The same Branding page controls primary, secondary, and accent colors, preset palettes including custom ones, theme mode, contrast, RTL, collapsed sidebar, and navigation color. Colors apply across the whole application, not just the storefront.
- 4
Customize online store content
Use Admin -> Online Store and Admin -> Content to manage menus, homepage sections, pages, blog posts, collections, and promotional content. The home page builder also sets desktop column counts for the New Arrivals and Top Articles grids.
- 5
Change packaged defaults
Edit config/branding.config.ts when you want the source package defaults to match your brand before seeding or deployment.
config/branding.config.tstsexport const DEFAULT_STORE_NAME = "Storify"; export const DEFAULT_CURRENCY = "USD"; export const DEFAULT_PRIMARY_COLOR = "#2065D1";
Theme defaults to light
The storefront ships light-first with dark available. Currency is admin-driven and language is URL-driven, so a visitor's locale comes from the path segment rather than a stored preference.
Storify ships 18 locales with RTL-ready routing. Locale files live under /locales and are loaded with next-intl; the active list is declared in config/i18n.config.ts.
- 1
Edit existing strings
Open /locales/<locale>.json and edit values directly. Changes hot-reload in development. English is the complete reference file; other locales vary in coverage and fall back to English for any key they do not define, so check the languages you plan to ship.
- 2
Add a new language
Copy /locales/en.json to /locales/<your-locale>.json, translate the values, then register the locale and its metadata in config/i18n.config.ts.
config/i18n.config.tstsexport const locales = [ "en", "bn", "ar", "es", "fr", "de", "tr", "hi", "nl", "zh", "ja", "zu", "xh", "af", "sw", "ha", "yo", "ig", ] as const; export const defaultLocale: Locale = "en"; - 3
Enable languages for the storefront
Admin -> Settings -> General -> Supported Languages controls which of the installed locales visitors can actually pick, and Default Language sets the fallback.
- 4
RTL support
Arabic is included and switches layout direction automatically. Test menus, checkout, product pages, POS, and dashboards in RTL before launch.
- 5
Currency settings
Admin -> Settings -> General sets the default currency and the supported list. Prices format with the currency's own regional conventions while keeping Latin digits, and each order stores the currency it was placed in so historical totals never get relabelled.
Vercel is the fastest path to production for most Storify buyers. Prepare environment variables, MongoDB Atlas, storage, and payment webhooks before launch.
- 1
Push to a GitHub repo
Create a private GitHub repo and push the source. Vercel will pull from it.
bashgit init git add . git commit -m "Initial Storify setup" git remote add origin git@github.com:you/storify.git git push -u origin main - 2
Import the repo into Vercel
vercel.com -> New Project -> Import. Vercel auto-detects Next.js.
- 3
Add environment variables
Project Settings -> Environment Variables. Paste the keys from .env.example. Set NEXT_PUBLIC_APP_URL and BETTER_AUTH_URL to your production URL, and add CRON_SECRET so the scheduled routes authenticate.
- 4
Connect MongoDB Atlas
Add Vercel's outbound access to MongoDB Atlas Network Access, then set MONGODB_URI in Vercel before deploying. Keep instances x MONGODB_MAX_POOL_SIZE under your Atlas plan's connection limit.
bashMONGODB_URI=mongodb+srv://username:password@cluster0.mongodb.net/storify - 5
Pick a storage provider that fits
Serverless filesystems are ephemeral, so use Cloudflare R2 or S3 on Vercel. The Local provider is for a VPS or container with a mounted volume.
- 6
Confirm the cron jobs
vercel.json already declares all four scheduled routes. Check them under Project -> Cron Jobs after the first deploy, and make sure CRON_SECRET is set in the same environment.
- 7
Update gateway and channel webhooks
Once the production domain is live, point Stripe, Razorpay, Paystack, Pesapal IPN, ioTec callback, the Meta webhook, and the Telegram webhook at it.
If you prefer your own server, Storify can run behind nginx or Caddy with a managed MongoDB connection or a self-hosted MongoDB container.
- 1
Write a Dockerfile first
The package ships source, not a container image, so there is no Dockerfile in the archive. Add a Node 20 multi-stage build that runs pnpm install and pnpm build, then pnpm start. Set output: "standalone" in next.config.ts if you want a slim runtime image.
Dockerfile (minimal)bashFROM node:20-alpine WORKDIR /app RUN corepack enable COPY . . RUN pnpm install --frozen-lockfile && pnpm build EXPOSE 3000 CMD ["pnpm", "start"] - 2
Build and start the stack
With the Dockerfile in place, build the images and run them in the background.
bashdocker compose up -d --build - 3
Seed only for evaluation
Load demo records only when you are setting up a test or evaluation instance.
bashdocker compose exec app pnpm db:seed - 4
Front with Caddy
Use nginx or Caddy to terminate TLS and proxy to localhost:3000. Caddy can provision Let's Encrypt automatically. Raise the proxy's upload body limit if you accept large 3D models or digital files.
Caddyfilenginxstorify.yourdomain.com { reverse_proxy localhost:3000 } - 5
Persist uploads if you use Local storage
The Local provider writes public media under public/uploads and digital product deliverables to private-uploads, which sits outside public/ so it is never statically served. Mount both so they survive a rebuild, and include both in your backups.
yamlvolumes: - storify-uploads:/app/public/uploads - storify-private:/app/private-uploads - 6
Schedule the cron routes yourself
There is no Vercel cron on a VPS. Add the four scheduled routes to system cron with the CRON_SECRET bearer token — see Scheduled Jobs for the exact intervals.
services:
app:
build: .
ports: ["3000:3000"]
env_file: .env
environment:
MONGODB_URI: mongodb://storify:change-me@mongo:27017/storify?authSource=admin
MONGODB_DB_NAME: storify
depends_on: [mongo]
mongo:
image: mongo:7
restart: unless-stopped
environment:
MONGO_INITDB_ROOT_USERNAME: storify
MONGO_INITDB_ROOT_PASSWORD: change-me
volumes:
- mongodata:/data/db
ports: ["27017:27017"]
volumes:
mongodata:Use a managed database when possible
Self-hosting MongoDB works, but you own backups, replication, and upgrades. MongoDB Atlas offloads that work.
Running more than one instance
Leave RATE_LIMIT_STORE unset so rate-limit counters live in MongoDB and a limit means the same thing everywhere. The in-memory store is only correct for exactly one instance.
Four routes must run on a schedule. They are ordinary authenticated GET endpoints, so any scheduler works: Vercel Cron, system cron, a container sidecar, or an external uptime scheduler.
messaging-outbox
Retries outbound channel messages whose immediate send failed. Skip it and a message that failed once is never delivered.
messaging-escalations
Emails whoever owns an unanswered conversation — the assigned agent, otherwise the vendor owner or their staff, falling back to the configured escalation address for platform-owned chats — once it passes the "Escalate after (minutes)" interval. That timer is wall-clock and does not pause outside business hours.
email-deliveries
Retries queued transactional email. Skip it and a temporary SMTP failure becomes a permanently lost password reset or order email.
vendor-subscriptions
Drives renewals, dunning retries before expiry, and plan expiry. Only needed if you sell vendor subscription plans.
{
"crons": [
{ "path": "/api/cron/email-deliveries", "schedule": "*/5 * * * *" },
{ "path": "/api/cron/vendor-subscriptions", "schedule": "0 * * * *" },
{ "path": "/api/cron/messaging-outbox", "schedule": "* * * * *" },
{ "path": "/api/cron/messaging-escalations", "schedule": "*/5 * * * *" }
]
}* * * * * curl -fsS -H "Authorization: Bearer $CRON_SECRET" https://yourdomain.com/api/cron/messaging-outbox
*/5 * * * * curl -fsS -H "Authorization: Bearer $CRON_SECRET" https://yourdomain.com/api/cron/messaging-escalations
*/5 * * * * curl -fsS -H "Authorization: Bearer $CRON_SECRET" https://yourdomain.com/api/cron/email-deliveries
0 * * * * curl -fsS -H "Authorization: Bearer $CRON_SECRET" https://yourdomain.com/api/cron/vendor-subscriptionsSet CRON_SECRET before scheduling
Each route compares the Authorization: Bearer header against the deployed CRON_SECRET. Use your scheduler's secret store rather than pasting the value into a command that lands in shell history or logs.
Once your app is live, route a custom domain to it. Both Vercel and Caddy can provision SSL certificates for free.
- 1
Add an A or CNAME record
For Vercel, add a CNAME pointing to cname.vercel-dns.com. For your VPS, point an A record at the server IP.
- 2
Verify in Vercel or Caddy
Vercel auto-verifies once DNS propagates. Caddy reissues the certificate on the next request.
- 3
Update application URLs
Switch NEXT_PUBLIC_APP_URL and BETTER_AUTH_URL to https://yourdomain.com and redeploy. These are used in emails, auth callbacks, and public metadata.
- 4
Update external integrations
Update payment webhooks and the Pesapal IPN registration, PayPal return and cancel URLs, the Meta and Telegram webhook callbacks, OAuth redirect URIs, the storage public URL, and email sender links to use the production domain.
Re-register the Pesapal IPN after a domain change
The stored IPN id points at the old URL. Open Admin -> Settings -> Payment Settings and choose Register IPN again, otherwise Pesapal notifications keep going to the previous domain.
After your first deploy, create real users and assign scoped roles for admin, vendors, staff/sellers, and customers.
- 1
Create the production admin
Use the create-admin script to create or upgrade a user to admin with a strong password.
bashpnpm create-admin admin@yourdomain.com "StrongPassword123!" "Store Admin" - 2
Configure multi-vendor mode
Use Admin -> Settings -> Multi-Vendor Management to enable or disable marketplace mode. In single-vendor mode, Storify uses the default main store vendor.
- 3
Approve vendors
Review vendor applications, approve or reject sellers, configure commission rates, assign subscription plans, and monitor payouts from the admin dashboard.
- 4
Invite staff and sellers
Create staff users for POS, inventory, orders, products, messaging, and customer workflows. Role-based permissions keep admin-only areas protected.
- 5
Scope staff to what they own
A staff member can be limited to a vendor, a POS location, or a region. That scope is applied by every list, detail loader, and API route alike, so a scoped staff member cannot reach an out-of-scope order by typing its id into the URL.
- 6
Set messaging permissions
Inbox access is its own permission. Grant it to the staff who answer conversations, and use conversation assignment, claim, and unassign to divide the work.
Do not share admin accounts
Shared logins break accountability. Create separate admin, staff, and vendor accounts for every real user — the order timeline and audit log record who did what.
Account status applies to everyone
Suspension and deactivation are enforced for every role, including admins and staff. Databases created before this run pnpm db:migrate:account-status once.
Storify defaults to safe rather than convenient: providers stay off until you enable them, two-factor is opt-in, and secrets are checked at startup. This is the pre-launch pass to make sure nothing was left open.
Secret preflight
A production build refuses to start on an empty, placeholder, or short BETTER_AUTH_SECRET. Generate real values for it, CRON_SECRET, MESSAGING_ENCRYPTION_KEY, and META_WEBHOOK_VERIFY_TOKEN.
Password policy and 2FA
Set the password policy in Security & Access Control. Two-factor is opt-in and can be required for admins and vendors once your team is enrolled.
OAuth stays off until you say so
Google and Facebook credentials in .env never auto-enable a provider. The admin toggle is the authority, so an admin can always turn a provider off.
Shared rate limiting
Limits are counted in MongoDB by default so they hold across instances. Tune the admin, vendor, checkout, cart, coupon, and auth presets in Security & Access Control.
Media delivery
Local media is protected against path traversal and uploaded SVG execution, trusted remote domains are centralized, and the chat media proxy is sandboxed rather than relaying provider content types on your origin.
Audit history
Order and admin actions are recorded for 24 months, long enough to outlive a card chargeback window. Existing databases apply the new retention with pnpm db:migrate:audit-indexes.
Pre-launch checklist
Rotate every seeded password, create a personal admin account, set the password policy, enable 2FA for admins, confirm gateway keys and webhooks are in live mode, and verify DEMO_MODE is false.
Keep Storify updated while protecting your database, uploaded media, custom translations, and brand customizations. Releases from 1.1 onward ship migrations that must run once against an existing database.
- 1
Back up first
Take a database backup and, if you use Local storage, a copy of the upload directory before merging anything. Every migration below has a dry-run twin, but a backup is the only real undo.
- 2
Download the latest build
Download updates from your CodeCanyon account and read the changelog for the versions between yours and the new one before merging.
- 3
Diff and merge
Use git or your favorite diff tool to merge new files. Review /locales, /config, /public, .env.example, and any customized components carefully — new releases add environment keys as well as code.
- 4
Install and rebuild
Install dependencies, then verify before deploying.
bashpnpm install pnpm typecheck pnpm test pnpm build - 5
Run the migrations for the versions you skipped
Run each dry pass, read its output, then apply. Running a migration twice is safe; skipping one leaves the feature it supports half-configured.
Upgrading to 1.3.1 from an earlier releasebash# 1.1 -> product & review indexes pnpm db:migrate:indexes # 1.2 -> barcodes (resolve duplicates reported by the dry run first) pnpm db:migrate:barcodes:dry pnpm db:migrate:barcodes # 1.3 -> performance, omnichannel, vendor billing, push, account status pnpm db:migrate:perf-indexes pnpm db:migrate:dashboard pnpm db:migrate:vendor-storefront-indexes pnpm db:migrate:omnichannel pnpm db:migrate:support-conversations # only if you used legacy support tickets pnpm db:migrate:telegram-keys # only if Telegram was connected before 1.3 pnpm db:migrate:vendor-billing pnpm db:migrate:push pnpm db:migrate:account-status # 1.3.1 -> audit retention 90 days to 24 months pnpm db:migrate:audit-indexes - 6
Re-check settings after upgrading
New releases add settings pages. After 1.3.1, review AI Configuration, Omnichannel Messaging, Notification Settings, the country availability policy, and your favicon on the Branding page — icons now come from settings rather than bundled files, so an install with nothing configured shows no icon at all.
- 7
Schedule database backups
Use MongoDB Atlas automated backups or set up nightly mongodump on a VPS.
bash# Cron: nightly backup at 02:00 0 2 * * * mongodump --uri="$MONGODB_URI" --archive=/backups/storify-$(date +\%F).archive --gzip - 8
Back up uploaded media
On R2 or S3, enable bucket lifecycle and versioning where possible and keep provider credentials safe. On Local storage, back up the upload directory — it holds digital deliverables as well as images.
Mongo will not change an existing TTL index
The audit retention change is the clearest example: without pnpm db:migrate:audit-indexes, an upgraded database keeps expiring order history at 90 days no matter what the new code says.
Most install problems fall into the same handful of buckets. Walk through these before opening a support ticket.
MONGODB_URI connection refused
Check username, password, database name, and Atlas Network Access allowlist. For self-hosted MongoDB, confirm authSource.
AI features unavailable
Check the key in Admin -> Settings -> AI Configuration (or OPENAI_API_KEY), run the connection test, and confirm the specific surface, plus staff or vendor access, is switched on and within its daily limit.
Payment succeeds but order stays pending
Confirm the gateway webhook URL and secret are configured in the same test or live mode as your keys, and that the endpoint is publicly reachable without authentication.
Vendor paid but the plan never activated
Check that /api/payments/webhook is subscribed to the vendor billing events, that the signing secret matches, and that /api/cron/vendor-subscriptions runs hourly with a valid CRON_SECRET.
Channel messages never arrive
Verify the Meta webhook URL and verify token, that META_APP_SECRET matches the app signing the request, and that MESSAGING_ENCRYPTION_KEY has not changed since the connection was saved.
Outbound messages or emails stall
Both retry through cron routes. Confirm messaging-outbox, messaging-escalations, and email-deliveries are scheduled and returning 200 rather than 401 from a stale CRON_SECRET.
Emails landing in spam
Set up SPF, DKIM, and DMARC for your sender domain. Resend and SendGrid have step-by-step UIs for this.
Product images or 3D models do not show
Check the storage provider settings, public media URL, bucket permissions, file MIME type, and next.config.ts image remotePatterns. After switching providers, confirm the public URL was saved rather than left on a stale draft.
A product reports out of stock incorrectly
Digital products and products with Track quantity off carry no stock count by design. If a physical product is wrong, check Continue selling when out of stock and the per-location inventory rather than the aggregate.
Vendor products missing from POS
Selling vendor-owned stock is opt-in. It needs pos.allowVendorProducts enabled, multi-vendor mode on, and a POS location selected on the register.
A country is missing at checkout
Admin -> Settings -> General holds the country availability policy. If it is set to an allow list, only those countries appear anywhere an address is entered, and the server rejects anything else.
Barcode migration reports duplicates
That is the dry run doing its job. Resolve each duplicate product or variant barcode in the catalog, then re-run the dry pass until it is clean before applying the migration.
pnpm install fails
Delete node_modules only, keep pnpm-lock.yaml, run corepack enable, then run pnpm install again.
Production build refuses to start
The startup preflight rejects an empty, placeholder, or under-32-character BETTER_AUTH_SECRET. Generate a real value with openssl rand -base64 32 and redeploy.
Deletes refused with a demo message
DEMO_MODE is true. Set it to false on a real store; the message shown in admin tables is the server's own refusal, not a generic failure.