v2.0.0 is stable - a theme engine and visual block builder, a second storefront template, offline POS, and one command for every migration.Read the 2.0 notes
logo
DocsDocumentation
⌘ K
Documentation·Store setup

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.

Chapter 01 · Overview

Welcome to Storify

Storify is a complete ecommerce platform: storefront, admin dashboard, vendor marketplace, point of sale, payments, shipping and AI tools. It is built with Next.js 16, React 19, Tailwind CSS 4, MongoDB and Better Auth. This documentation takes you from the zip you downloaded to a live store, and then explains each feature's settings.

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, Orange Money, MTN MoMo, 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.

Carrier shipping

Shippo (global) and Shiprocket (India) rate-shop a parcel, buy a real label, and keep tracking in sync — manually from the order page or automatically once an order is paid.

Finance & ledger

A double-entry ledger behind profit and loss, cash position, expenses, receivables, vendor statements, month-end closing, and CSV export.

Product boosting

Sell a ladder of numbered sponsored positions priced per day. Vendors book a rung for a range of days; off by default, and marketplace-only.

Storage choices

Cloudflare R2, DigitalOcean Spaces, AWS S3, or self-hosted MinIO — all S3-compatible and served by the same code — with a provider-aware Media Library and automatic WebP conversion.

Start here

New to Storify? Go to Choose Your Path. It compares running Storify on your computer, on Dokploy, on Vercel and on a plain VPS, and sends you to the right step-by-step guide.

Already running an earlier release?

Read Updates & Migrations first. Some releases include database migrations that must run once on your existing database.

Chapter 02 · Choose Your Path

Pick how you will install Storify

You can run Storify on your own computer to try it, or put it online with Dokploy, Vercel or a plain VPS. Every path ends at the same five-step installer in your browser. Pick one path. Then prepare your accounts and values in Before You Install, and follow only your path's chapter.

Try it on your computer

Evaluate

Run a private test store on your Windows, Mac or Linux computer. Only you can open it, so use it to look around before you pay for hosting.

  • Suits: trying Storify before you go live
  • Costs: nothing extra (MongoDB Atlas Free, or MongoDB on your computer)
  • Skill: install Node.js and type a few commands
  • Time for a first store: roughly 30–60 minutes
Open this guide

Dokploy on your own server

Recommended

Dokploy is a free control panel that you install on a Linux server you rent (a VPS). From its web page you deploy the store, add your domain with HTTPS, read logs and schedule jobs.

  • Suits: most live stores
  • Costs: the VPS (8 GB RAM, or 4 GB plus swap) and a domain
  • Skill: a few commands over SSH, then a web panel
  • Time for a first store: roughly 1–2 hours
Open this guide

Vercel

No server

Vercel builds and hosts the app for you, so there is no server to look after. A live store needs the Pro plan: the free Hobby plan is for non-commercial use and cannot deploy Storify's scheduled jobs.

  • Suits: owners who never want to manage a server
  • Costs: Vercel Pro (from $20/month), MongoDB Atlas and a domain
  • Skill: GitHub and web dashboards
  • Time for a first store: roughly 1 hour
Open this guide

A plain VPS

Full control

You install Node.js, the app, a web server and the scheduled jobs yourself on a Linux server. You control every part, and you maintain every part.

  • Suits: developers and agencies who know Linux
  • Costs: the VPS (4 GB RAM plus swap, or more) and a domain
  • Skill: Linux command line (SSH, systemd, crontab)
  • Time for a first store: roughly 2–3 hours
Open this guide

Before you start

  • Before You Install walks you through every item below. Do it after you pick your path.
  • The zip you downloaded from CodeCanyon. The app folder inside it is the one that contains package.json.
  • A MongoDB database. MongoDB Atlas Free is fine to start. On Dokploy, MongoDB can also run inside the panel.
  • A storage bucket for uploads, for example Cloudflare R2 (Storage & Media). You can also tick "Set storage up later" in the installer.
  • For a live store: a domain you control, such as shop.your-domain.com, so the store gets HTTPS. On Vercel you can start with the free .vercel.app address.
  • For Vercel and Dokploy: a private GitHub repository with the Storify files. They deploy from it.
  • Six settings you enter on every path: MONGODB_URI, MONGODB_DB_NAME, BETTER_AUTH_SECRET, BETTER_AUTH_URL, NEXT_PUBLIC_APP_URL and CRON_SECRET. Vercel and Dokploy each add one more.
  • An SMTP email account, so the store can send password resets and staff invites. You can add it after the install (Email / SMTP).

Compare the four paths

PathBackground jobsShell for commandsUpdating
Your computerNot set up. That is fine for testing.Yes, your own terminalReplace the files, run pnpm install, restart pnpm dev
DokployYou add 8 schedules in the panel once. Together they run all 14 jobs.Yes, Open Terminal in the panelPush the new release to GitHub. Dokploy rebuilds by itself.
VercelAutomatic on Pro: vercel.json sets up all 14No. You run commands from your own computer.Push the new release to GitHub. Vercel rebuilds by itself.
Plain VPSYou add 14 crontab lines onceYes, over SSHUpload the new files, then pnpm install, pnpm build and restart

Background jobs are 14 tasks the store runs on a timer, such as retrying emails and checking payments (Scheduled Jobs). A shell is a command line on the machine that runs the store; you need it now and then for maintenance commands. Server sizes are in Requirements. Before you install a new release on any path, read Updates & Migrations.

Shared hosting and cPanel are not supported

Storify is a Node.js app that must keep running, next to a MongoDB database and 14 scheduled jobs. Shared hosting and cPanel plans are not made for this, and Storify does not support them. Use one of the four paths above.

Run the installer right after you deploy

On a new store, whoever opens /install first creates the owner account. Open it yourself as soon as the first deploy finishes, before you share the address. See First Run & Installer.

Not sure? Start locally, then deploy

Install Storify on your computer first with Set Up Your Computer, or with the Quickstart if Node.js, pnpm and MongoDB are already installed. When you like what you see, come back here and pick a production path. The live store uses its own database and runs the installer again.

Chapter 03 · Quickstart

Run a test store on your computer

This is the short local path for people who already have Node.js 24, pnpm and MongoDB. It gives you a private test store on your own computer, in roughly 15 minutes. If any of these tools are new to you, follow Set Up Your Computer instead: it covers the same steps in more detail.

Before you start

  • Node.js 24 LTS (22.12.0 at least): node -v prints a version that starts with v24.
  • pnpm 10.24.0 through Corepack: in the app folder, pnpm -v prints 10.24.0. If it does not, see Set Up Your Computer.
  • A MongoDB you can connect to: MongoDB on your computer (for example the Docker command in Database Setup) or a MongoDB Atlas cluster.
  • The app folder from the zip you downloaded: the folder that contains package.json.
  1. 1

    Install the packages

    Open a terminal in the app folder and install the packages Storify needs. The first install downloads a lot and can take a few minutes.

    1. 1Windows: open the app folder in File Explorer, click the address bar, type cmd and press Enter.
    2. 2macOS or Linux: open Terminal, type cd and a space, drag the app folder into the window, and press Enter.
    3. 3Run the command below.
    bash
    pnpm install
    An error that starts with ERR_PNPM_ usually means the wrong pnpm version, or hidden files (such as pnpm-workspace.yaml) that were lost while copying the folder. Set Up Your Computer shows how to fix both.

    Check: The output ends with a line that starts with Done in. Yellow WARN lines are normal. A line with ERR_PNPM is not.

  2. 2

    Generate two secrets

    Storify needs two long random values: one protects sign-in (BETTER_AUTH_SECRET) and one protects the scheduled jobs (CRON_SECRET). Run the command below twice and copy each result.

    Windows, macOS and Linux
    bash
    node -e "console.log(require('crypto').randomBytes(32).toString('hex'))"
    macOS / Linux alternative
    bash
    openssl rand -hex 32
    Paste the printed value, never the command itself. Keep both values in a password manager.

    Check: Each run prints a different line of 64 characters: digits and the letters a to f.

  3. 3

    Create the .env file

    The .env file holds your settings. Create it in the app folder, in the same terminal. Payment, email and storage keys come later; you do not need them now.

    1. 1Windows: run notepad .env. If Notepad asks whether to create a new file, click Yes.
    2. 2macOS: run touch .env, then open -e .env to open it in TextEdit.
    3. 3Linux: run nano .env. Save with Ctrl+O and Enter, close with Ctrl+X.
    4. 4Paste the block below. Replace both <…> placeholders, including the < and > signs, with your two secrets. Save the file.
    .env
    txt
    MONGODB_URI=mongodb://localhost:27017/storify
    MONGODB_DB_NAME=storify
    BETTER_AUTH_SECRET=<the first 64-character secret>
    BETTER_AUTH_URL=http://localhost:3000
    NEXT_PUBLIC_APP_URL=http://localhost:3000
    CRON_SECRET=<the second 64-character secret>
    Using MongoDB Atlas? Replace the first line with your Atlas connection string, with /storify before the ?, and allow your current IP address in Atlas. Keep MONGODB_DB_NAME=storify either way. See Database Setup.

    Check: dir .env (Windows) or ls -a .env (macOS and Linux) lists a file named exactly .env. Windows lists .env.txt instead? Rename it with ren .env.txt .env.

  4. 4

    Start the app

    Start the development server and leave this terminal open while you use the store. Press Ctrl+C in it to stop the store. After you change .env, stop it and run pnpm dev again.

    bash
    pnpm dev
    Want the full demo store (products, orders and demo logins) instead of the installer? Run pnpm db:seed before you start the app and before you open the site for the first time. Then sign in at http://localhost:3000/login with the logins it prints, and change their passwords. The seed locks the installer, so never run it on a live store's database. Details: Database Setup.

    Check: The terminal shows http://localhost:3000 next to Local:, then a line with Ready in.

  5. 5

    Run the installer

    Open http://localhost:3000 in your browser. A new store sends you to /install. The first page can take a minute to appear. Work through the five screens; First Run & Installer explains each one.

    1. 1System check: the Node.js, MongoDB connection and Authentication secret rows must pass. Click Continue.
    2. 2Create your admin account: your name, email and a password of at least 8 characters. This account becomes the store's Owner.
    3. 3Store basics: store name, language and currency. Untick "Multi-vendor marketplace" if you run a single store.
    4. 4Where media is stored: enter your bucket details, or tick "Set storage up later".
    5. 5Choose your storefront template: keep the demo data box ticked for a sample catalog, then click Install.
    A row failed in the System check? Fix the value in .env, restart pnpm dev, then click "Check again".

    Check: You see "Your store is ready". Click "Go to sign in" and sign in with your new admin account; http://localhost:3000/admin opens the dashboard.

This is a test store

Only your computer can open http://localhost:3000, and no scheduled jobs run. When you are ready to sell, pick a production path in Choose Your Path. The live store gets its own database and its own installer run.

Chapter 04 · Requirements

What Storify needs to run

Storify is one Node.js app that keeps running, one MongoDB database and one storage bucket for uploads. This page lists the software versions and the machine size each path needs. Building the app takes much more memory than running the store, so the build decides how big a server must be.

Software versions

ComponentVersionNotes
Node.js24 LTS recommended; 22.12.0 at leastThe bundled .nvmrc pins 24. The installer's System check refuses anything older than 22.12. Node.js 25 and newer no longer include Corepack: if corepack is "not found", run npm install -g corepack first.
pnpm10.24.0 exactlyRun corepack enable once. pnpm then uses the version named in package.json: in the app folder, pnpm -v prints 10.24.0. On Windows, if it reports a permission error, run the terminal as Administrator. Do not use npm or yarn: the lockfile works with pnpm only.
MongoDB7.0 or newerMongoDB Atlas (the Free tier is fine to start) or your own MongoDB server. The Docker command in Database Setup runs MongoDB 7. A replica set is not required.
StorageOne S3-compatible bucketCloudflare R2, AWS S3, DigitalOcean Spaces or MinIO. Storify does not keep uploads on the server's disk.
Operating system64-bit Linux on serversThe server guides use Ubuntu 22.04 or 24.04 LTS. For testing, your own computer can run Windows, macOS or Linux.
HTTPSRequired in productionWeb push notifications and payment webhooks need an https:// address. Vercel, Dokploy and Caddy (the web server in the VPS guide) get free certificates for you.

Machine size for each path

PathMemory (RAM)CPUDiskNotes
Your computer8 GB or moreAny recent 64-bit processorAbout 5 GB freeEnough for pnpm dev. The project's packages alone take about 1 GB.
VercelHandled by VercelHandled by VercelHandled by VercelVercel builds and runs the app on its own machines. You still need the Pro plan, MongoDB Atlas and a bucket.
Dokploy, build on server8 GB, or 4 GB plus 4 GB swap2 vCPU or more30 GB or moreDokploy alone needs 2 GB RAM and 30 GB disk. On a 4 GB server, add swap and the build caps from Deploy with Dokploy.
Dokploy, prebuilt image4 GB is safer; 2 GB at least (Dokploy's own minimum)2 vCPU30 GB or moreGitHub Actions builds the app, so the server only runs it. An advanced option at the end of Deploy with Dokploy.
Plain VPS4 GB plus 4 GB swap (8 GB is more comfortable)2 vCPU or more20 GB or moreOn a 4 GB server, use the build caps from Deploy to a VPS.

These sizes are rough guides, not hard limits. Running MongoDB on the same server? Add about 1 GB of RAM for it. Uploads live in the bucket, so they do not use server disk. Swap is disk space that Linux uses as extra memory; a vCPU is one processor core in a VPS plan. Still choosing? Compare the paths in Choose Your Path.

The build is the heavy part

Building the app (pnpm build) is the most memory-hungry job a server ever does. Node.js may use up to 4 GB during the build, and the compiler needs more on top of that. On a small server, add swap and the build caps (BUILD_MAX_CPUS=2 and BUILD_TURBOPACK_MEMORY_MB=3072 on a 4 GB server) instead of raising that limit. Without swap, a build that runs out of memory can make the whole server stop responding, often with a "Bad Gateway" page. With swap, the worst case is a slow build.

The build works without the database

pnpm build does not need to reach MongoDB, and it does not download fonts from the internet. When it cannot reach the database, the build log shows several MongoDB connection error lines (for example with ENOTFOUND) and [sitemap] … emitting hub pages only lines. They are expected and harmless: the build still finishes, and the sitemap fills in once the running store reaches the database. Still set MONGODB_URI before the first deploy, because the running store needs it straight away.

14 scheduled jobs must run

Storify has 14 scheduled jobs: tasks that run on a timer, such as retrying failed emails, handing paid orders to the carrier, checking mobile-money payments and marking abandoned checkouts. On Vercel Pro they run by themselves. On Dokploy or a VPS you set them up once. See Scheduled Jobs.

Windows is fine for testing

pnpm dev runs in Command Prompt and PowerShell. pnpm build fails there with 'NODE_OPTIONS' is not recognized as an internal or external command until you run pnpm config set shell-emulator true once, or use WSL2. Servers should run Linux.

Optional services

OpenAI for the AI tools, a Meta developer app for WhatsApp, Messenger and Instagram, a Telegram bot, Shippo or Shiprocket for carrier labels, and QZ Tray for direct thermal printing. Each stays off until you set it up. Email (SMTP) is different: set it up before launch, because password resets need it (Email / SMTP).

Vercel needs the Pro plan

The free Hobby plan cannot deploy Storify as shipped. The deployment fails with Hobby accounts are limited to daily cron jobs. This cron expression would run more than once per day. Hobby is also for non-commercial use only. A live store needs Pro (from $20/month; a 14-day trial is available). See Deploy to Vercel.

Storage is not optional

Storify keeps uploads (product images, 3D models, downloads and chat files) in one S3-compatible bucket, not on the server's disk. Nobody can upload anything until a bucket is connected. You may tick "Set storage up later" in the installer, but connect the bucket before you add products. Most uploads go from the browser straight to the bucket, so the bucket also needs a CORS rule: a bucket setting that allows uploads from your store's address. See Storage & Media.

Chapter 05 · Before You Install

Get these ready before you install

Dokploy, Vercel, a VPS or your own computer: every path needs the same few things. Do them once here and write each value down in one private place. Your path chapter then tells you exactly where to paste them. A step that your path does not need says so.

Before you start

  • Your Storify purchase on CodeCanyon.
  • Node.js 24 LTS on your computer. You need it for the secret command below. Install it with the step "Install Node.js 24" in Set Up Your Computer, then come back.
  • A free GitHub account if you deploy with Vercel or Dokploy: sign up at github.com.
  • A free Cloudflare account for the storage bucket, or an account with another S3-compatible storage provider.
  • A domain name for a live store, for example your-domain.com. You can test without one: on your computer, or on Vercel's free .vercel.app address. Dokploy and a VPS need a domain for HTTPS.
  • A password manager, or another private place, for the values you create in this chapter.
  1. 1

    Download and unzip the package

    The app folder is the folder you install. It contains hidden files (their names start with a dot). They must stay with it wherever you copy or upload it.

    1. 1Sign in at codecanyon.net, open your account menu (your user name, top right) and click Downloads.
    2. 2Find Storify, click Download and choose All files & documentation (the label may differ slightly). Save the zip file.
    3. 3From the same Download menu, also save the License certificate & purchase code. Support asks for your purchase code.
    4. 4Unzip the file. Windows: right-click it → Extract All… → Extract. macOS: double-click it. Linux: run unzip followed by the file name.
    5. 5If you find another zip file inside, unzip that one too. Open folders until you see the app folder: the one that contains package.json, vercel.json, .env.example and next.config.ts.
    6. 6Move the app folder to a place you will keep, for example C:\storify on Windows or a storify folder in your home folder on macOS. Do not use a folder that OneDrive, iCloud or Dropbox syncs: syncing thousands of small files slows everything down and can break the install.
    7. 7Show hidden files. Windows 11: File Explorer → View → Show → click Hidden items, and also File name extensions. Windows 10: View tab → tick Hidden items and File name extensions. macOS Finder: press Cmd+Shift+. (period).
    Keep every hidden file when you copy, zip or upload the folder: .env.example, .nvmrc, .pnpmfile.cjs, .gitignore and the .github folder. Keep pnpm-workspace.yaml and pnpm-lock.yaml too. Without .pnpmfile.cjs or pnpm-workspace.yaml the install stops with ERR_PNPM_LOCKFILE_CONFIG_MISMATCH.

    Check: Inside the app folder you see package.json, pnpm-lock.yaml, pnpm-workspace.yaml, .pnpmfile.cjs and .env.example. Files that start with a dot missing? Turn on hidden files (last item above) and look again. Still missing? Download the zip again, or contact support.

  2. 2

    Create the MongoDB database

    MongoDB is the database that holds your products, orders and users. Most stores use MongoDB Atlas, a hosted MongoDB with a free tier. Dokploy users may run MongoDB inside Dokploy instead (Deploy with Dokploy), and local testers may run MongoDB on their own computer with Docker or MongoDB Community Server (Database Setup). If you choose one of those, skip this step and the next one.

    1. 1Sign up at MongoDB Atlas. Answer or skip the welcome questions.
    2. 2Create a cluster (Atlas's name for a database server) and choose the Free tier: 512 MB of data, no automatic backups. It is fine for testing and a first launch. For a busy live store, use a paid tier or scheduled backups.
    3. 3Choose the region closest to where the store will run: your server's location (Dokploy, VPS) or your customers (Vercel). Create the cluster. This takes a few minutes.
    4. 4Atlas now asks you to create a database user (it may suggest a name and password). Use a user name such as storify_app and a password with letters and digits only, then create the user. Save the password in your password manager now. If Atlas does not ask, open Security → Database & Network Access → Database Users → Add New Database User.
    5. 5Open Security → Database & Network Access → IP Access List tab → Add IP Address. This list says which computers may connect to the database.
    6. 6Enter the entry for your path from the list below, then click Save and Close. Testing locally and deploying later? Add both entries.
    Local (your computer)
    Your current IP address. Atlas may have added it already while you created the cluster. If not, click the Add Current IP Address button. If your home IP address changes later, add the new one.
    Dokploy or VPS
    Your server's public IP address, for example 203.0.113.10. Your VPS provider's dashboard shows it.
    Vercel
    0.0.0.0/0 (the Allow Access from Anywhere button). Vercel has no fixed IP address, so this is required.
    Atlas emails every project member an alert about a 0.0.0.0/0 entry. This is expected. The database is still protected by its user name and password. The Atlas labels may differ slightly from the ones here.

    Check: The IP Access List tab shows your entry, and the Database Users tab lists your user.

  3. 3

    Build your MongoDB connection string

    The connection string tells Storify where the database is and how to sign in to it. Atlas gives you most of it. You add the password and the database name.

    1. 1In Atlas, open Clusters, click Connect on your cluster, then choose Drivers.
    2. 2Copy the connection string it shows and paste it into your notes.
    3. 3Replace <db_password> with your database user's password. Remove the < and > too. If you also see <db_username>, replace it with your user name.
    4. 4Make the part after .mongodb.net read /storify?. Atlas strings usually contain .mongodb.net/?: type storify between the / and the ?.
    User name
    Your database user, for example storify_app. Atlas usually fills it in.
    Password
    Replaces <db_password>. Change special characters as shown in the note below.
    Cluster address
    For example cluster0.xxxxx.mongodb.net. Keep it exactly as Atlas wrote it. Every cluster has its own.
    Database name
    storify. You add it yourself, between .mongodb.net/ and ?.
    Options
    ?retryWrites=true&w=majority. Keep these, and anything else Atlas added, such as &appName=Cluster0.
    The finished string looks like this
    txt
    mongodb+srv://USER:PASSWORD@cluster0.xxxxx.mongodb.net/storify?retryWrites=true&w=majority
    Special characters in the password break the string. Replace them: @ → %40, : → %3A, / → %2F, # → %23, ? → %3F, % → %25. Or give the database user a password with letters and digits only.

    Check: Your string starts with mongodb+srv://, has no < or > left, and contains .mongodb.net/storify?. It ends like …/storify?retryWrites=true&w=majority (an extra &appName=Cluster0 is fine).

  4. 4

    Generate your secrets

    Storify needs two long random values. One protects sign-in sessions. The other protects the background jobs. The command below makes them and works on Windows, macOS and Linux.

    1. 1Open a terminal (the window where you type commands). Windows: press Start, type cmd and press Enter. macOS: open Terminal from Applications → Utilities.
    2. 2Paste the first command below and press Enter. It prints one line of 64 characters.
    3. 3Copy that line into your notes as the value of BETTER_AUTH_SECRET.
    4. 4Run the command again. Save the new line as CRON_SECRET. The two values must be different.
    5. 5Later, if you connect chat channels, run it a third time for MESSAGING_ENCRYPTION_KEY (see "Write down your environment values" below).
    Windows, macOS and Linux (uses Node.js)
    bash
    node -e "console.log(require('crypto').randomBytes(32).toString('hex'))"
    macOS and Linux only (alternative)
    bash
    openssl rand -hex 32
    Save the line the command prints. Never save the command itself or a placeholder such as <64 characters>. If the terminal says node is not recognized or not found, install Node.js first: see Set Up Your Computer.

    Check: Each value is exactly 64 characters long and uses only the digits 0–9 and the letters a–f. The two values are different.

  5. 5

    Decide your store address

    Two variables, BETTER_AUTH_URL and NEXT_PUBLIC_APP_URL, will both hold this one address. Use https://, put no slash at the end, and write it exactly as people will type it. https://www.your-domain.com and https://your-domain.com are two different addresses, so pick one.

    Local (your computer)
    http://localhost:3000. This is the only address that uses http://.
    Vercel
    Start with https://<project-name>.vercel.app, using the project name you will type in Vercel. Vercel may add a suffix if the name is taken, so you check the real address after the first deploy. Later you move both variables to your own domain: Deploy to Vercel shows how.
    Dokploy or VPS
    The domain or subdomain you will point at your server, for example https://shop.your-domain.com.
    Sign-in works only on this exact address. Anywhere else it fails with 403 "Invalid origin": on the www version when you chose the address without www (or the other way round), on another Vercel address, or on http instead of https. If you change the address later, change both variables and rebuild. Your path chapter shows how.

    Check: You have one address written down. It starts with https:// (or is http://localhost:3000) and ends right after the domain name, with no / at the end.

  6. 6

    Write down your environment values

    Environment variables are the settings Storify reads when it starts: each one is a NAME=value line. Put your production values into your private note in the format shown below. Your path chapter tells you where to paste them. A local install puts the same names in a .env file with http://localhost:3000 as the address: see Environment Variables.

    MONGODB_URI
    Your finished connection string from the step above, with the password already encoded. Dokploy users with a Dokploy MongoDB service get their string in Deploy with Dokploy.
    MONGODB_DB_NAME
    storify, the same name as in your connection string. Always set it. If it is missing and the connection string has no database name, the store quietly uses a database called test, and pnpm create-admin refuses to run.
    BETTER_AUTH_SECRET
    Your first secret. It protects sign-in sessions. Spell the name exactly like this: the installer checks only this name.
    BETTER_AUTH_URL
    Your store address.
    NEXT_PUBLIC_APP_URL
    The same address. It is built into the app when the app is built, so after changing it you must rebuild, not only restart.
    CRON_SECRET
    Your second secret. The background jobs send it to prove they may run. Without it every job is refused (error 401).
    Vercel adds
    ENABLE_EXPERIMENTAL_COREPACK=1. It makes Vercel install with exactly pnpm 10.24.0.
    Dokploy adds
    NIXPACKS_NODE_VERSION=24. It makes the Dokploy build use Node.js 24.
    Later: MESSAGING_ENCRYPTION_KEY
    Only when you connect WhatsApp, Messenger, Instagram or Telegram, or when vendors save their own carrier accounts. Use a third 64-character secret. Never change it afterwards, or the saved connections can no longer be read.
    Production values (Dokploy, Vercel, VPS)
    bash
    MONGODB_URI=mongodb+srv://USER:PASSWORD@cluster0.xxxxx.mongodb.net/storify?retryWrites=true&w=majority
    MONGODB_DB_NAME=storify
    BETTER_AUTH_SECRET=<64 characters from the command above>
    BETTER_AUTH_URL=https://your-store-url
    NEXT_PUBLIC_APP_URL=https://your-store-url
    CRON_SECRET=<a different 64 characters>
    Do not paste the whole .env.example file into a hosting panel. Its placeholder values count as real: fake STORAGE_* keys make the installer skip the storage step, ADMIN_PASSWORD=change-me becomes the password pnpm create-admin gives, and CRON_SECRET=replace-with-… passes as a real secret. Enter only the variables you fill in yourself.

    Check: Your note has the six NAME=value lines (plus the extra line for Vercel or Dokploy). No < or > is left, there are no spaces around =, and both URL lines hold the same address.

  7. 7

    Put the source in a private GitHub repository

    Vercel and Dokploy build your store from a GitHub repository (an online copy of the app folder). It must be private, because Storify is licensed source code. A local install skips this step; on a VPS it is optional. Do not use GitHub's upload page: it accepts at most 100 files at a time, and Storify has about 3,100.

    1. 1Install GitHub Desktop and sign in with your GitHub account.
    2. 2Click File → Add local repository and choose the app folder (the one with package.json).
    3. 3GitHub Desktop says the folder is not a Git repository. Click the "create a repository" link, leave the options as they are, and click Create repository. GitHub Desktop saves all files in a first commit (a saved version) by itself.
    4. 4Click Publish repository. Keep "Keep this code private" ticked, then click Publish repository.
    5. 5Prefer the command line instead of GitHub Desktop? You need Git. On github.com click + → New repository, name it storify, choose Private, add no README, .gitignore or license, and click Create repository.
    6. 6In a terminal, go to the app folder, for example cd C:\storify (Windows) or cd ~/storify (macOS). Run the commands below, with your GitHub user name in place of YOUR-USERNAME.
    Command line (instead of GitHub Desktop)
    bash
    git init
    git add .
    git commit -m "Storify"
    git branch -M main
    git remote add origin https://github.com/YOUR-USERNAME/storify.git
    git push -u origin main
    First time using Git on the command line? If git commit says "Author identity unknown", run git config --global user.name "Your Name" and git config --global user.email "you@example.com", then commit again. On Windows, many lines like LF will be replaced by CRLF are harmless warnings. When git push asks you to sign in, approve it in the browser, or use a personal access token as the password.

    Check: On github.com the repository shows the Private label, lists package.json, vercel.json and .env.example at the top level (not inside a subfolder), and has no .env file. GitHub may also email you that a workflow named Deploy image failed. It runs on every push but is used only by Dokploy Path B (Deploy with Dokploy). If you do not use Path B, ignore it or turn it off: Actions tab → Deploy image → ⋯ → Disable workflow.

  8. 8

    Create a storage bucket

    Product images and other uploads are not kept on your server. They live in a bucket: online file storage that works like Amazon S3 ("S3-compatible"). Cloudflare R2 is the usual choice. Other providers are in Storage & Media. The installer lets you tick "Set storage up later", but nothing can be uploaded until storage is set, so do it now or right after the install.

    1. 1In the Cloudflare dashboard, open R2 (under Storage & databases). The first time, Cloudflare asks you to add R2 to your account through a short checkout.
    2. 2Click Create bucket and name it, for example storify-media (lowercase letters, digits and hyphens).
    3. 3Open the bucket → Settings. Under Public Development URL, click Enable, type allow and confirm. Copy the public URL it shows, like https://pub-xxxx.r2.dev.
    4. 4Go back to the R2 overview page. Under Account Details, copy your Account ID, then click Manage next to API Tokens.
    5. 5Create an Account API token with the permission Object Read & Write, limited to your bucket. Copy the Access Key ID and the Secret Access Key. The secret is shown only once.
    6. 6Open the bucket → Settings → CORS Policy → Add CORS policy. On the JSON tab, paste the rule below, put your store address in place of https://your-store-url, and click Save. Testing locally too? Add "http://localhost:3000" as a second origin.
    Bucket name
    For example storify-media.
    Access key ID
    From the API token.
    Secret access key
    From the API token. Save it now: Cloudflare does not show it again.
    Account ID
    32 characters, from the R2 overview page.
    Public URL
    For example https://pub-xxxx.r2.dev, or your own domain. Without it, images do not show.
    CORS rule for the bucket
    json
    [
      {
        "AllowedOrigins": ["https://your-store-url"],
        "AllowedMethods": ["PUT", "GET", "HEAD"],
        "AllowedHeaders": ["content-type"],
        "ExposeHeaders": ["ETag"],
        "MaxAgeSeconds": 3600
      }
    ]
    R2 is free up to a monthly allowance, but the checkout may still ask for a card or PayPal. Cloudflare limits traffic to r2.dev addresses and says they are for testing. For a live store, it recommends connecting your own domain to the bucket instead (Storage & Media). The CORS rule lets the admin upload large files straight to the bucket. The Cloudflare labels may differ slightly from the ones here.

    Check: You have saved the five values above, and the bucket's Settings page shows your CORS policy. The installer's Test connection button checks them later.

  9. 9

    Continue with your path

    You now have everything the install needs. Open the chapter for your path. Each one ends with the First Run & Installer and the Launch Checklist.

    1. 1Dokploy, a control panel on your own server: Deploy with Dokploy.
    2. 2Vercel, with no server to manage (a live store needs the Pro plan): Deploy to Vercel.
    3. 3A plain Linux server that you manage yourself: Deploy to a VPS.
    4. 4Your own computer, to try Storify out: Set Up Your Computer.
    5. 5Not sure yet? Compare the paths in Choose Your Path.

Keep your values private

The secrets, the MongoDB password and the storage keys give full control of your store. Keep them in a password manager, not in email, chat, screenshots or support tickets. Never put them in Git: the bundled .gitignore keeps .env files out of the repository, so leave that file as it is. Vercel's Secret variables cannot be read back later, so your copy may be the only one.

Testing first? Use a second database name

The installer runs only once per database. If a local test and your live store share one database, the live store skips the installer and opens with your test data. Use storify for the live store and another name, for example storify-dev, for tests. Change it in both MONGODB_URI and MONGODB_DB_NAME.

Chapter 06 · Set Up Your Computer

Install Node, pnpm and the project

Do this once on the computer where you will run Storify. You need it for a local install. You also need it if you host on Vercel: Vercel has no terminal, so maintenance commands such as pnpm create-admin or pnpm db:migrate run from your own computer. You install Node.js 24, turn on pnpm, and download the packages the project needs.

Before you start

  • The unzipped Storify download. "Download and unzip the package" in Before You Install shows how to get it.
  • Windows 10 or 11, macOS or Linux, and permission to install programs.
  • A few GB of free disk space. The project's packages alone take about 1 GB.
  • An internet connection for the downloads.
  1. 1

    Find the app folder

    The app folder is the one that contains package.json. Every command in this chapter runs in that folder.

    Check: The app folder contains package.json, pnpm-lock.yaml, pnpm-workspace.yaml, .pnpmfile.cjs, .nvmrc and .env.example. Files whose names start with a dot are hidden on macOS and Linux: press Cmd+Shift+. in Finder, or Ctrl+H in most Linux file managers, to see them.

  2. 2

    Install Node.js 24

    Node.js is the program that runs Storify. Install version 24 LTS (LTS means long-term support). Storify needs at least Node.js 22.12.

    1. 1Open nodejs.org/en/download.
    2. 2In the version menu, pick the version that starts with v24 and is marked LTS, for example v24.21.0. Do not pick a higher number, such as v26.
    3. 3Windows: click Windows Installer (.msi). macOS: click macOS Installer (.pkg). The labels may differ slightly. Linux: use the nvm commands in the second box below instead.
    4. 4Run the installer and keep the default options. On Windows, one screen is called Tools for Native Modules: leave its box unticked. Storify does not need those tools.
    5. 5Close every terminal window that is already open. A terminal is the window where you type commands.
    6. 6Open a new terminal. Windows: press Start, type cmd and press Enter. macOS: open Terminal from Applications → Utilities.
    7. 7Run node -v.
    Check the version
    bash
    node -v
    Linux (or macOS): install with nvm instead
    bash
    # Ubuntu without curl? First run: sudo apt install curl
    curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.40.8/install.sh | bash
    # close the terminal, open a new one, then:
    nvm install 24
    nvm alias default 24
    node -v
    Already have Node.js? Run node -v first. If it prints v22.12.0 or higher, keep it and go to the next step.

    Check: node -v prints v24 followed by more numbers, for example v24.21.0. Any version from v22.12.0 works. Version 25 or newer needs one extra command in the next step.

  3. 3

    Turn on pnpm

    pnpm is the tool that downloads Storify's packages. Node.js includes a helper called Corepack that fetches the exact pnpm version the project asks for, 10.24.0. You turn it on once.

    1. 1Run corepack enable in any terminal window.
    2. 2Windows: if you see a permission error (EPERM), open Command Prompt as administrator (Start → type cmd → right-click Command Prompt → Run as administrator). Run corepack enable there, then close that window.
    3. 3macOS or Linux: if you see a permission error (EACCES), run sudo corepack enable and type your computer password.
    4. 4If the terminal says corepack is not recognized or not found, you have Node.js 25 or newer, which no longer includes Corepack. Run npm install -g corepack first (on macOS or Linux, put sudo in front if it reports a permission error), then corepack enable.
    bash
    corepack enable

    Check: corepack enable prints nothing when it works. You check the pnpm version in "Install the project's packages" below.

  4. 4

    Open a terminal in the app folder

    From here on, every command must run inside the app folder, the one with package.json.

    1. 1Windows: open the app folder in File Explorer, click the address bar at the top, type cmd and press Enter. A Command Prompt opens in that folder.
    2. 2macOS: open Terminal, type cd and a space, drag the app folder from Finder into the Terminal window, then press Enter.
    3. 3Linux: right-click inside the folder in your file manager → Open in Terminal, or cd into the folder.
    Check that you are in the right folder
    bash
    # Windows
    dir package.json
    
    # macOS or Linux
    ls package.json
    Keep this window open. You use it for the rest of this chapter and for Environment Variables.

    Check: The command lists package.json. If it says File Not Found, Cannot find path or No such file or directory, you are in the wrong folder.

  5. 5

    Install the project's packages

    First check the pnpm version. Then pnpm downloads everything Storify needs into a new node_modules folder. The first install can take several minutes.

    1. 1Run pnpm -v. The first time, Corepack may ask Do you want to continue? [Y/n] before it downloads pnpm. Type Y and press Enter.
    2. 2Run pnpm install and wait until you can type again.
    bash
    pnpm -v
    pnpm install
    ERR_PNPM_UNSUPPORTED_ENGINE means the Node.js or pnpm version is wrong. Check node -v. If pnpm -v did not print 10.24.0, remove any pnpm you installed another way (for example with npm uninstall -g pnpm), run corepack enable again and open a new terminal. ERR_PNPM_LOCKFILE_CONFIG_MISMATCH means hidden files such as .pnpmfile.cjs or pnpm-workspace.yaml were lost while copying: unzip the download again with hidden files shown.

    Check: pnpm -v prints 10.24.0, and pnpm install ends with a line like Done in … using pnpm v10.24.0. A node_modules folder now sits next to package.json. Yellow WARN lines are normal. A line with ERR_PNPM is not.

Use pnpm only

The lockfile, pnpm-lock.yaml, is for pnpm. Do not run npm install or yarn in the app folder: they ignore the lockfile and install different package versions.

What comes next

Local install: create your .env file in Environment Variables, then connect MongoDB in Database Setup. Hosting on Vercel? This computer is now ready for maintenance commands: go back to Deploy to Vercel. Came here only to install Node.js for the secret command? "Install Node.js 24" is enough: go back to Before You Install.

Chapter 07 · Environment Variables

Set your environment variables

Environment variables are settings Storify reads when it starts: where the database is, the secret that protects sign-ins, and the store's web address. On your computer they live in a plain text file named .env in the app folder. On Vercel or Dokploy you type them into the hosting panel instead. Six values are enough to start. Everything else is optional, and most of it can also be set later in the admin panel.

Before you start

  • For a local install: Node.js installed and pnpm install finished in the app folder. See Set Up Your Computer.
  • Your two secrets from "Generate your secrets" in Before You Install. The first step below shows the command again.

The values every install needs

VariableLocal value (.env)Production value (hosting panel)
MONGODB_URImongodb://localhost:27017/storify for MongoDB in Docker or installed on your computer, or your Atlas stringYour Atlas string, for example mongodb+srv://USER:PASSWORD@cluster0.xxxxx.mongodb.net/storify?retryWrites=true&w=majority. On Dokploy you may use its own MongoDB service instead (Deploy with Dokploy).
MONGODB_DB_NAMEstorifystorify
BETTER_AUTH_SECRETYour first secret (64 characters)Your first secret (64 characters)
BETTER_AUTH_URLhttp://localhost:3000https://your-store-url: the exact address people open, with no / at the end
NEXT_PUBLIC_APP_URLhttp://localhost:3000The same address as BETTER_AUTH_URL
CRON_SECRETYour second secret, different from the firstYour second secret, different from the first
ENABLE_EXPERIMENTAL_COREPACKNot needed1, on Vercel only
NIXPACKS_NODE_VERSIONNot needed24, on Dokploy only

The first six rows are required on every install. The last two apply to one platform each. Sign-in works only on the exact address in the two URL variables: on any other address it fails with 403 "Invalid origin".

  1. 1

    Have your two secrets ready

    BETTER_AUTH_SECRET and CRON_SECRET need long random values. Use the two you made in "Generate your secrets" in Before You Install. If you have none yet, run this command twice. It works on Windows, macOS and Linux.

    bash
    node -e "console.log(require('crypto').randomBytes(32).toString('hex'))"
    Run the command once for every secret you need, now or later (for example MESSAGING_ENCRYPTION_KEY). Copy the line it prints, never the command itself. Save each value in a password manager.

    Check: Each run prints one line of 64 characters, only digits and the letters a to f. Your two values are different.

  2. 2

    Create the .env file (local install)

    In the terminal that is open in the app folder, create a file named exactly .env and open it in a text editor.

    1. 1Windows: run notepad .env. If Notepad asks whether to create a new file, click Yes.
    2. 2macOS: run touch .env and then open -e .env. The file opens in TextEdit.
    3. 3Linux: run nano .env. Save with Ctrl+O and Enter, close with Ctrl+X.
    Windows hides file extensions, so a file saved as .env.txt still looks like .env in File Explorer. Storify ignores that file. The dir .env check below shows the real name.

    Check: dir .env (Windows) or ls -a .env (macOS and Linux) lists a file named exactly .env, in the same folder as package.json.

  3. 3

    Fill in the six local values

    Paste this block into .env. Replace both <64 characters> placeholders, including the < and >, with your two secrets, and save the file. Start from this block, not from a copy of .env.example.

    .env (local)
    bash
    MONGODB_URI=mongodb://localhost:27017/storify
    MONGODB_DB_NAME=storify
    BETTER_AUTH_SECRET=<64 characters>
    BETTER_AUTH_URL=http://localhost:3000
    NEXT_PUBLIC_APP_URL=http://localhost:3000
    CRON_SECRET=<64 characters>
    Keep MONGODB_URI as shown if MongoDB runs on your computer (Docker or Community Server). Using MongoDB Atlas? Put your Atlas string there instead. Database Setup explains both. Whenever you change .env later, stop pnpm dev with Ctrl+C and start it again.

    Check: The file has six lines in the form NAME=value, with no spaces around = and no < or > left. Later, the installer's System check shows the Authentication secret row in green.

  4. 4

    Production: enter the values in your hosting panel

    On a server you use the production values from your notes ("Write down your environment values" in Before You Install). Vercel and Dokploy have no .env file to edit: you enter the values in the hosting panel, before the first deploy.

    Vercel
    Settings → Environment Variables. Add ENABLE_EXPERIMENTAL_COREPACK=1 too. See Deploy to Vercel.
    Dokploy
    The application's Environment tab. Add NIXPACKS_NODE_VERSION=24 too. See Deploy with Dokploy.
    VPS
    A .env file in the app folder on the server. See Deploy to a VPS.
    Enter only the variables you fill in yourself. Do not paste the whole .env.example: the app treats its placeholders as real values. The fake STORAGE_* keys make the installer skip the storage step, ADMIN_PASSWORD=change-me becomes the password pnpm create-admin gives, and CRON_SECRET=replace-with-a-long-random-secret passes as a real secret.

    Check: The panel lists all six names, spelled exactly as in the table above, plus the extra one for Vercel or Dokploy. No value still contains < or >.

  5. 5

    Rebuild after you change a NEXT_PUBLIC_ value

    Values whose names start with NEXT_PUBLIC_ are copied into the app when it is built. After you change one, a restart is not enough: rebuild as shown here.

    Your computer
    Stop pnpm dev with Ctrl+C and start it again. If you use pnpm start, run pnpm build first.
    Vercel (rebuild)
    Deployments → ⋯ next to the latest deployment → Redeploy, with "Use existing Build Cache" unticked.
    Dokploy (rebuild)
    Click Deploy on the application's General tab.
    VPS (rebuild)
    Run pnpm build, then sudo systemctl restart storify.
    Other values, such as BETTER_AUTH_URL, need only a restart: on Vercel a Redeploy, on Dokploy a Reload, on a VPS sudo systemctl restart storify.
KeyRequiredDescription
MONGODB_URIRequiredWhere the MongoDB database is. Local: mongodb://localhost:27017/storify. Atlas: mongodb+srv://USER:PASSWORD@cluster0.xxxxx.mongodb.net/storify?retryWrites=true&w=majority. Percent-encode special characters in the password (@ becomes %40, : becomes %3A) or use letters and digits only. Without it, every page that reads the database fails.
MONGODB_DB_NAMERequiredThe database name. Always set it to storify. Without it, an Atlas string that has no /storify makes the app use a database called test, while pnpm create-admin refuses to run.
BETTER_AUTH_SECRETRequiredProtects sign-in sessions. Use a 64-character value from the secret command. The installer blocks while it is empty, shorter than 32 characters or a known placeholder, and in production sign-in fails without it. Name it exactly BETTER_AUTH_SECRET: the installer checks only this name. .env.example leaves it empty.
BETTER_AUTH_URLRequiredThe exact address people open the store on: https, no slash at the end (local: http://localhost:3000). Used for sign-in callbacks, email links and trusted origins. Sign-in on any other address fails with 403 Invalid origin. It is read when the app starts, so after a change a restart is enough.
NEXT_PUBLIC_APP_URLRequiredThe same address as BETTER_AUTH_URL. It is also the base for links in emails, canonical and sitemap addresses, and some payment and shipping callbacks; Pesapal needs a public https value. It is built into the app, so after a change you must rebuild: Vercel Redeploy, Dokploy Deploy, VPS pnpm build and restart.
CRON_SECRETRequiredProtects the 14 scheduled jobs. Every job refuses a request without it (401 Unauthorized), so abandoned-checkout emails, carrier hand-offs, payment checks and the other background jobs never run. Use a 64-character value from the secret command, different from BETTER_AUTH_SECRET. .env.example ships replace-with-a-long-random-secret, which the app accepts as a real secret, so always replace it. Vercel sends it automatically; on Dokploy and a VPS your schedules send it.
ENABLE_EXPERIMENTAL_COREPACKOptionalVercel only, and required there. Not in .env.example. Set it to 1 so Vercel installs with exactly pnpm 10.24.0. Without it the install can stop with ERR_PNPM_UNSUPPORTED_ENGINE.
NIXPACKS_NODE_VERSIONOptionalDokploy (Nixpacks build) only; set it there. Not in .env.example. Set it to 24 so the build uses Node.js 24.
MONGODB_MAX_POOL_SIZEOptionalHow many database connections one app instance may keep open. Default 50, or 5 on Vercel. Keep the number of instances times this value under your MongoDB plan's connection limit (Atlas M0: 500). Commented out in .env.example.
MONGODB_AUTO_INDEXOptionalLeave it unset for the first start: the app then checks its roughly 370 database indexes every time it starts and creates any that are missing. Once the store is set up you may set false in production, which saves that work on every start (useful on Vercel). From then on, run the index migrations yourself when you upgrade. Commented out in .env.example.
MONGOOSE_DEBUGOptionalFor development only. true logs every database query; stack logs the code that sent it. Never set it in production: it is very verbose and logs query values. Commented out in .env.example.
RATE_LIMIT_STOREOptionalWhere API rate-limit counters live. Unset keeps them in MongoDB, so limits hold across several app instances. Set memory only if exactly one instance runs. Commented out in .env.example.
NEXT_PUBLIC_APP_NAMEOptionalFallback store name used in transactional emails and notifications until a store name is saved in Admin → Settings → General Settings. It does not change the browser tab title or page metadata.
NEXT_PUBLIC_SUPPORT_EMAILOptionalSupport address shown on the access-denied page. Order emails and invoices use the store email from Admin → Settings → General Settings instead.
NEXT_PUBLIC_ENABLE_PWA_IN_DEVOptionalTurns on the service worker (used by the installable app and push notifications) during pnpm dev. Production builds turn it on by themselves. .env.example ships true; leave it out unless you are testing these features.
BUILD_MAX_CPUS / BUILD_NODE_HEAP_MB / BUILD_TURBOPACK_MEMORY_MBOptionalMemory limits for pnpm build only; the running store ignores them. BUILD_NODE_HEAP_MB defaults to 4096. On a server that also runs the store, add swap and set BUILD_MAX_CPUS=2 and BUILD_TURBOPACK_MEMORY_MB (for example 3072 on a 4 GB server). Commented out in .env.example.
BUILD_SOURCE_MAPSOptionaltrue adds server source maps to the build: about 190 MB more and a slower build. Off by default. Turn it on only to upload maps to an error tracker. Commented out in .env.example.
ADMIN_NAMEOptionalFallback display name for pnpm create-admin when you leave out the name argument.
ADMIN_PASSWORDOptionalFallback password for pnpm create-admin when you leave out the password argument. .env.example ships change-me, which then becomes the real password, and for an existing email the command resets that user's password to it. Leave this line out and always type a strong password in the command: pnpm create-admin you@shop.com "StrongPassword" "Your Name".
OPENAI_API_KEYOptionalPowers 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_ENABLEDOptionalOpt-in switch for the admin home page hero banner generator. Defaults to disabled.
SMTP_HOSTOptionalSMTP server host name. Needed for password resets, staff invites and order emails, unless you set email up in Admin → Settings → Email Configuration (SMTP).
SMTP_PORTOptionalSMTP port, usually 587 for STARTTLS or 465 for implicit TLS.
SMTP_USEROptionalSMTP user name from your email provider.
SMTP_PASSOptionalSMTP password or API key. Server-only; it never reaches the browser.
SMTP_FROMOptionalDefault From header, for example "Storify <no-reply@example.com>". The sending domain should have SPF, DKIM and DMARC set up.
TWILIO_ACCOUNT_SID / TWILIO_AUTH_TOKEN / TWILIO_MESSAGING_SERVICE_SID / TWILIO_FROM_NUMBEROptionalTwilio credentials for SMS notifications. Twilio bills every message. Set a Messaging Service SID (recommended) or a From number. SMS stays off until an admin turns it on in Admin → Settings → SMS (Twilio). Commented out in .env.example.
WEB_PUSH_PUBLIC_KEYOptionalPublic VAPID key for browser push subscriptions. Generate the key pair with pnpm push:keys.
WEB_PUSH_PRIVATE_KEYOptionalPrivate VAPID key the server uses to sign push messages. Keep it secret.
WEB_PUSH_SUBJECTOptionalVAPID subject, usually a mailto: address or your store address.
NEXT_PUBLIC_WEB_PUSH_PUBLIC_KEYOptionalThe same public VAPID key, given to the browser so the service worker can subscribe.
MESSAGING_ENCRYPTION_KEYOptionalAt least 32 characters; make it with the secret command. Encrypts the stored tokens of WhatsApp, Messenger, Instagram and Telegram connections. If it is missing or shorter, every channel action fails with MESSAGING_ENCRYPTION_KEY is not configured. .env.example ships a placeholder that is long enough to pass, so replace it before you connect a channel. Never change it afterwards: the saved connections could no longer be read.
META_APP_IDOptionalMeta developer app ID used by WhatsApp embedded signup and Messenger connections.
META_APP_SECRETOptionalMeta app secret used to verify the signature of every incoming webhook before it is read.
META_WHATSAPP_CONFIGURATION_IDOptionalFacebook Login for Business configuration ID for WhatsApp embedded signup. Without it, the manual WABA, phone and token form is the fallback.
META_INSTAGRAM_CONFIGURATION_IDOptionalOptional second Login for Business configuration that grants Instagram messaging permissions. Without it, the Instagram panel falls back to the manual Page-token form.
META_WEBHOOK_VERIFY_TOKENOptionalA long random value (the secret command works) that you also enter as the verify token in your Meta app's webhook setup.
META_GRAPH_API_VERSIONOptionalThe Graph API version pinned for your Meta app, for example v21.0.
GOOGLE_CLIENT_ID / GOOGLE_CLIENT_SECRETOptionalGoogle sign-in credentials. Credentials alone do not turn the provider on: the toggle in Admin → Settings → OAuth / Social Login decides.
FACEBOOK_APP_ID / FACEBOOK_APP_SECRETOptionalFacebook sign-in credentials, also switched on by the toggle in Admin → Settings → OAuth / Social Login.
STRIPE_SECRET_KEY / STRIPE_WEBHOOK_SECRETOptionalStripe server credentials for checkout payments and vendor subscription billing. NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY is the browser-side key.
PAYPAL_CLIENT_ID / PAYPAL_CLIENT_SECRET / PAYPAL_WEBHOOK_IDOptionalPayPal credentials and webhook ID used to capture and verify orders.
PAYSTACK_SECRET_KEY / PAYSTACK_PUBLIC_KEYOptionalPaystack keys. The public key is read from PAYSTACK_PUBLIC_KEY or NEXT_PUBLIC_PAYSTACK_PUBLIC_KEY.
RAZORPAY_KEY_ID / RAZORPAY_KEY_SECRET / RAZORPAY_WEBHOOK_SECRETOptionalRazorpay credentials. The key ID is read from RAZORPAY_KEY_ID or NEXT_PUBLIC_RAZORPAY_KEY_ID.
PESAPAL_MODE / PESAPAL_CONSUMER_KEY / PESAPAL_CONSUMER_SECRET / PESAPAL_IPN_IDOptionalPesapal API 3.0 credentials, sandbox or live mode, and the IPN ID you get when you register the notification URL from the admin settings.
IOTEC_MODE / IOTEC_CLIENT_ID / IOTEC_CLIENT_SECRET / IOTEC_WALLET_IDOptionalioTec Pay client credentials and wallet ID for mobile money and card payments in Uganda.
STORAGE_ACCESS_KEY_ID / STORAGE_SECRET_ACCESS_KEYOptionalS3-compatible keys for Cloudflare R2, DigitalOcean Spaces, AWS S3 or MinIO. Needed in practice: without storage nothing can be uploaded. When STORAGE_BUCKET and both keys are filled, the installer skips its storage step, even if they are the .env.example placeholders.
STORAGE_ENDPOINT / STORAGE_REGION / STORAGE_BUCKET / STORAGE_ACCOUNT_IDOptionalBucket details. R2: account ID plus region auto. Spaces: the datacenter region, endpoint left blank. S3: the bucket region. MinIO: the API endpoint URL on port 9000, region us-east-1.
STORAGE_PUBLIC_URLOptionalPublic base URL or CDN domain that serves uploaded files. Needed for R2: without it, images point at the API endpoint and return 403. CLOUDFLARE_R2_PUBLIC_URL is still read as an older fallback.
SHIPPO_MODE / SHIPPO_TEST_TOKEN / SHIPPO_LIVE_TOKEN / SHIPPO_WEBHOOK_SECRETOptionalShippo carrier credentials. Shippo has no separate test server: the token decides whether a label is real, so both tokens are stored and the mode picks one.
SHIPROCKET_EMAIL / SHIPROCKET_PASSWORD / SHIPROCKET_PICKUP_LOCATION / SHIPROCKET_WEBHOOK_TOKENOptionalShiprocket API-user login, the pickup-location nickname registered in their dashboard, and the webhook token. India only, and there is no sandbox: any working login is a live account.
CARRIER_ENCRYPTION_KEYOptionalNot in .env.example: add it yourself if you want it. Encrypts carrier tokens that vendors save for their own carrier accounts. When it is unset, the app uses MESSAGING_ENCRYPTION_KEY. Set it before any vendor connects a carrier account, and never change it afterwards.
NEXT_PUBLIC_GA_ID / NEXT_PUBLIC_GTM_ID / NEXT_PUBLIC_FACEBOOK_PIXEL_ID / NEXT_PUBLIC_TIKTOK_PIXEL_IDOptionalAnalytics and pixel IDs for the browser. Also configurable in Admin → Settings → Analytics Integration. The .env.example placeholders count as real IDs, so leave these out unless you have real ones.
PLAUSIBLE_API_KEYOptionalServer-side Plausible key used by the admin analytics dashboard.
QZ_TRAY_CERTIFICATE / QZ_TRAY_PRIVATE_KEYOptionalSigned QZ Tray certificate and key for direct thermal printing. Only the server's signing route reads the private key; never make it public.
DEMO_MODEOptionalNot in .env.example: add it yourself for a public demo. true (or 1, yes, on) turns demo mode on; NEXT_PUBLIC_DEMO_MODE works the same way. Deletes, settings changes and test actions are then refused, creating products and uploading images still work, and the carrier jobs skip their work. Leave it unset on a real store.

Two sources of configuration

Payments, social login, SMTP, SMS, storage, analytics and AI credentials can be set here or in Admin → Settings. A value saved in Admin → Settings always wins, so you can start from environment variables and later change single fields in the admin panel. Messaging is the exception: MESSAGING_ENCRYPTION_KEY and the META_* variables are read from the environment only.

Chapter 08 · Database Setup

Set up MongoDB

Storify keeps everything in MongoDB: products, orders, customers, users and settings. This chapter is for a local install: you need a running MongoDB and its address in your .env file. Pick one of the three options below. The app creates its collections and indexes by itself the first time it starts, so there is nothing to import. Deploying to a server instead? Your deploy chapter covers the database.

Before you start

Where MongoDB can run

OptionGood forWhat you installMONGODB_URI
A. MongoDB Atlas (recommended)Testing on your computer, and later the live storeNothing: a free account on the MongoDB websitemongodb+srv://USER:PASSWORD@cluster0.xxxxx.mongodb.net/storify?retryWrites=true&w=majority
B. DockerA quick local test if you already use DockerDocker Desktopmongodb://localhost:27017/storify
C. MongoDB Community ServerA local test without Docker or a cloud accountMongoDB itself, running in the backgroundmongodb://localhost:27017/storify

A single MongoDB server is enough: Storify does not need a replica set. Whatever you choose, also set MONGODB_DB_NAME=storify.

  1. 1

    Option A: use MongoDB Atlas

    Atlas is MongoDB's own cloud service, and its Free tier is enough for testing. Create the cluster, the database user and the connection string as described in "Create the MongoDB database" and "Build your MongoDB connection string" in Before You Install.

    1. 1Allow your own computer: Security → Database & Network Access → IP Access List → Add IP Address → Add Current IP Address → Confirm. The exact labels may differ slightly.
    2. 2If your internet provider gives you a new IP address later, the connection stops working. Add the new address the same way.
    3. 3Will the same Atlas cluster hold your live store later? Test with another database name, for example storify-dev, in both MONGODB_URI and MONGODB_DB_NAME. The installer runs only once per database.
    Special characters in the database password break the string. Replace them (@ → %40, : → %3A, / → %2F, # → %23, ? → %3F, % → %25) or give the user a password with letters and digits only.

    Check: You have a connection string like mongodb+srv://USER:PASSWORD@cluster0.xxxxx.mongodb.net/storify?retryWrites=true&w=majority, with your real password in place of PASSWORD and /storify before the ?.

  2. 2

    Option B: run MongoDB in Docker

    Docker runs programs in isolated containers. Use this option if Docker Desktop is installed and running (get Docker Desktop). One command downloads MongoDB 7 and starts it on port 27017.

    bash
    docker run -d --name storify-mongo -p 27017:27017 mongo:7
    After you restart your computer, open Docker Desktop and start the database again with docker start storify-mongo. The data stays until you remove the container. Your MONGODB_URI is mongodb://localhost:27017/storify.

    Check: docker ps lists a container named storify-mongo whose status starts with Up.

  3. 3

    Option C: install MongoDB Community Server

    This installs MongoDB directly on your computer as a background service. Your MONGODB_URI is then mongodb://localhost:27017/storify.

    1. 1Windows: open the MongoDB download page, choose Platform Windows and Package msi, click Download and run the file.
    2. 2Windows: choose Complete, make sure Install MongoD as a Service is ticked, keep the default service settings and click Install. MongoDB Compass, a visual database browser, is optional.
    3. 3macOS: install Homebrew from brew.sh if you do not have it yet, then run the three commands below.
    4. 4Linux: follow MongoDB's guide for your distribution.
    macOS (Homebrew)
    bash
    brew tap mongodb/brew
    brew install mongodb-community
    brew services start mongodb-community

    Check: Windows: the Services app (Start → type Services) shows the MongoDB service as Running. macOS: brew services list shows mongodb-community as started.

  4. 4

    Put the connection string in .env

    Open .env in the app folder, set these two lines and save. If pnpm dev is already running, stop it with Ctrl+C and start it again.

    MONGODB_URI (Docker or Community Server)
    mongodb://localhost:27017/storify. It is already in your .env if you used the block from Environment Variables.
    MONGODB_URI (Atlas)
    Your Atlas string, for example mongodb+srv://USER:PASSWORD@cluster0.xxxxx.mongodb.net/storify?retryWrites=true&w=majority.
    MONGODB_DB_NAME
    The same name as in your MONGODB_URI: storify, or your test name (for example storify-dev) if you chose one in Option A. This value wins over the name in the string. Set it with every option: without it, an Atlas string with no database name makes the app use a database called test.
    The terminal shows ECONNREFUSED when the app starts? MongoDB is not running on your computer. Docker: open Docker Desktop and run docker start storify-mongo. Community Server: start the MongoDB service (Windows: the Services app; macOS: brew services start mongodb-community).

    Check: When you start the app in Running Locally, the installer's System check shows the MongoDB connection row in green.

  5. 5

    Optional: load the demo store instead of the installer

    pnpm db:seed fills the database with a complete demo store and demo logins, and the installer then stays closed. Run it now, before you open the site for the first time. "Instead of the installer (developers)" in First Run & Installer explains all the options.

    bash
    pnpm db:seed
    # or another template:
    pnpm db:seed --template=women-fashion
    The demo passwords are public: use the seed for a private test only. If it stops with a message that ends in nothing was written, the database already holds other data: use an empty database name, for example storify-demo, in both MONGODB_URI and MONGODB_DB_NAME.

    Check: Near the end the output shows Database seeded successfully! and a Demo Credentials: list, for example admin@storify.com / Admin@123.

Never seed or reset a live store

Never run pnpm db:seed, pnpm db:seed:users, pnpm db:reset or pnpm db:full-reset against a production database. The seed commands add demo accounts with publicly known passwords. The reset commands delete every order, customer and setting without asking. To start over on a test database, see "Locked out, or /install shows 404" in First Run & Installer.

Atlas Free has no automatic backups

The Free tier (M0, 512 MB) is fine for testing and a first launch, but Atlas makes no backups of it. For a busy live store, move to a paid tier with backups, or schedule your own mongodump backups.

New installs need no migrations

Every time the app starts, it checks its roughly 370 database indexes and creates any that are missing, and the installer sets up the rest. pnpm db:migrate only updates an existing store when you upgrade to a new release: see Updates & Migrations. If you later set MONGODB_AUTO_INDEX=false on a busy live store, you run the index migrations yourself on each upgrade.

Chapter 09 · Running Locally

Start Storify on your computer

Start the store on your own computer, open it in your browser and run the installer. Use this to try Storify or to work on the code. A live store runs on a server instead: see the Deploy to Production chapters.

Before you start

  • Set Up Your Computer is done: Node.js 24, pnpm, and pnpm install finished in the app folder.
  • Your .env file sits in the app folder, next to package.json, with the values from Environment Variables.
  • Your database is ready, as in Database Setup: MongoDB Atlas with your IP address allowed, or MongoDB running in Docker or on this computer.
  1. 1

    Start the dev server

    The dev server runs the whole store on this computer: storefront, admin, vendor and staff areas. It runs only while its terminal window stays open.

    1. 1Open a terminal in the app folder (the folder with package.json). On Windows, open the folder in File Explorer, click the address bar, type cmd and press Enter.
    2. 2Run pnpm dev.
    3. 3Wait for the line that starts with ✓ Ready in. Leave this window open.
    4. 4If Windows asks whether Node.js may use your networks, allowing private networks is enough. http://localhost:3000 works either way.
    bash
    pnpm dev
    What the terminal shows (your numbers will differ)
    txt
    ▲ Next.js 16.3.4 (webpack)
    - Local:         http://localhost:3000
    - Network:       http://192.168.1.20:3000
    - Environments: .env
    ✓ Ready in 3.2s
    If the terminal says Port 3000 is in use … using available port 3001 instead, another program already uses port 3000. Often it is an older pnpm dev window. Close that program, press Ctrl+C here and run pnpm dev again. Your .env addresses say port 3000, so the store must run there.

    Check: The Local: line shows http://localhost:3000, the Environments: line lists .env, and ✓ Ready in … appears. No Environments: line? Then Storify did not find your .env file: check that it sits next to package.json and is not named .env.txt.

  2. 2

    Open the store in your browser

    Open http://localhost:3000. Type localhost, as in your .env, not 127.0.0.1. The first page can take a minute to appear, because the dev server prepares each page the first time you open it.

    Not sent to /install? An error page, or a page that loads for a long time and then fails, means Storify cannot reach your database. Pages do not redirect then. Open http://localhost:3000/install yourself: the "MongoDB connection" row shows what to fix. If the storefront opens instead, or /install shows 404, this database already has an admin. Did you load the demo store with pnpm db:seed in Database Setup? Then this is expected: skip "Run the installer" and sign in at http://localhost:3000/login with the demo logins it printed, for example admin@storify.com / Admin@123. Otherwise see "Locked out, or /install shows 404" in First Run & Installer.

    Check: The address changes to http://localhost:3000/install, and the page says "Set up your store" with "System check" below it.

  3. 3

    Run the installer

    Complete the five installer screens as described in First Run & Installer. On this computer the Application URL row is green when both URL values in .env are http://localhost:3000. No storage bucket yet? Tick "Set storage up later" on the storage screen.

    Do not run pnpm create-admin or pnpm db:seed before the installer. Both create an admin account, and the installer then closes for good.

    Check: You are signed in, and http://localhost:3000/admin shows the admin dashboard. A notification titled "Background jobs have stopped" is expected here: nothing runs the scheduled jobs on your computer.

  4. 4

    Try a production build (optional)

    pnpm dev is for trying and editing. A server runs a production build instead. You can test one here. The build can take several minutes.

    1. 1Stop pnpm dev with Ctrl+C. The production server needs port 3000 too.
    2. 2Windows: before your first build, run pnpm config set shell-emulator true once in this terminal. Without it, pnpm build stops with 'NODE_OPTIONS' is not recognized as an internal or external command.
    3. 3Run pnpm build. Wait until it ends with a long list of routes.
    4. 4Run pnpm start, then open http://localhost:3000.
    5. 5Changed a NEXT_PUBLIC_ value in .env later? Run pnpm build again. Those values are fixed when the app is built.
    Build, then start
    bash
    pnpm build
    pnpm start
    Lines such as MongoDB connection error … or [sitemap] … emitting hub pages only mean the build could not reach the database, for example because MongoDB is not running. They are harmless: the build still finishes. Start the database before you run pnpm start.

    Check: The build log shows ✓ Compiled successfully in … and ends with a list of routes. pnpm start prints ✓ Ready in …, and http://localhost:3000 opens your store.

  5. 5

    Check types and run tests (developers only)

    You need this only if you change the code. pnpm build does not check types, so run these yourself before you deploy your changes.

    bash
    pnpm typecheck
    pnpm test
    More memory for typecheck: macOS and Linux
    bash
    NODE_OPTIONS=--max-old-space-size=8192 pnpm typecheck
    More memory for typecheck: Windows Command Prompt
    cmd
    set NODE_OPTIONS=--max-old-space-size=8192
    pnpm typecheck
    More memory for typecheck: Windows PowerShell
    powershell
    $env:NODE_OPTIONS="--max-old-space-size=8192"; pnpm typecheck
    If pnpm typecheck stops with JavaScript heap out of memory, run it with more memory, using the box for your system. Storify's own automated checks give it 8 GB.

    Check: pnpm typecheck finishes without listing errors, and pnpm test ends with a summary of passed tests.

  6. 6

    Stop the server

    Click the terminal window where the server runs and press Ctrl+C. Your store's data stays in the database. To continue later, open a terminal in the app folder and run pnpm dev again.

    In Command Prompt you may see Terminate batch job (Y/N)?. Type Y and press Enter.

    Check: The terminal shows its normal prompt again, and http://localhost:3000 no longer opens.

Running a public demo

Want a demo site where visitors can try the admin? On the server that hosts the demo, load the demo store with pnpm db:seed into an empty database before anyone opens the site (Database Setup). Then add DEMO_MODE=true to the environment and restart. Deletes, settings edits and test actions are refused, and the sign-in page shows the demo logins to everyone. Never set DEMO_MODE on a real store.

Chapter 10 · Deploy with Dokploy

Deploy Storify with Dokploy, step by step

Dokploy is a free control panel that you install on your own server. From your browser, it builds Storify from your private GitHub repository, runs it, gets the HTTPS certificates and shows the logs. This chapter takes you from an empty Ubuntu server to a live store with its background jobs running.

Pick your server size first

Building on the server needs memory

By default, Dokploy builds Storify on your server. For a few minutes the build needs several GB of memory, on top of the running store and the panel. Choose one path:

  • Path A (recommended): steps 1–16. Dokploy builds the store from your GitHub repository. You need 8 GB of RAM, or 4 GB plus 4 GB of swap (step 1) and the two BUILD_ limits (step 8).
  • Path B (advanced): GitHub Actions builds a ready-to-run image, so the server never builds and 4 GB of RAM is enough. Do steps 1–9, but skip step 4, and in step 7 only create the application. Then do steps 17–18 instead of step 10, and continue at step 11.

Before you start

  • A fresh VPS with Ubuntu 22.04 or 24.04, and SSH access as root (or as a user with sudo). Nothing else installed: ports 80, 443 and 3000 must be free. Do not reuse a server you set up with Deploy to a VPS, or one that runs Caddy, nginx or Apache.
  • Server size: at least 30 GB of disk (Dokploy's minimum), and 8 GB of RAM or 4 GB plus swap (step 1). Path B works with 4 GB.
  • If your VPS provider has a cloud firewall, allow ports 22, 80 and 443, and 3000 for the first login.
  • A domain where you can add DNS records. You will point two names at the server: one for the panel (for example panel.your-domain.com) and one for the store (for example shop.your-domain.com).
  • Storify in a private GitHub repository: see the step "Put the source in a private GitHub repository" in Before You Install.
  • Two different 64-character secrets, for BETTER_AUTH_SECRET and CRON_SECRET: see "Generate your secrets" in Before You Install.
  • A storage bucket for product images (Storage & Media), or the plan to add one right after the installer. Nothing can be uploaded until storage is set up.
  1. 1

    Connect to the server and add swap

    Log in to the server from a terminal on your computer. Then add 4 GB of swap: disk space that Linux uses as extra memory, so a build that runs out of RAM gets slower instead of crashing the server.

    1. 1Windows: open PowerShell (press Start and type PowerShell). It has ssh built in. macOS and Linux: open Terminal.
    2. 2Run ssh root@SERVER-IP, replacing SERVER-IP with the IP address from your VPS provider (for example ssh root@203.0.113.10). If your provider gave you a user such as ubuntu, run ssh ubuntu@SERVER-IP instead.
    3. 3The first time, type yes to trust the server. Then type the password if asked. Nothing appears on the screen while you type it; that is normal.
    4. 4Run free -h. If the Swap line already shows 4.0Gi or more, skip the commands below.
    5. 5Otherwise, paste the commands below and press Enter. To paste in PowerShell, right-click or press Ctrl+V.
    Add 4 GB of swap
    bash
    sudo fallocate -l 4G /swapfile
    sudo chmod 600 /swapfile
    sudo mkswap /swapfile
    sudo swapon /swapfile
    echo '/swapfile none swap sw 0 0' | sudo tee -a /etc/fstab
    free -h
    Add swap even on an 8 GB server. With swap, the worst case is a slow build. Without it, a build that fills the memory can freeze the whole server, including the live store.

    Check: The last free -h shows a Swap line with a total of 4.0Gi or more (more if your provider had already added some swap).

  2. 2

    Install Dokploy and create your owner account

    One command installs Docker, the Dokploy panel and Traefik, the web server that sends visitors to the right app and gets its HTTPS certificate. Then you register in the panel. The first person who opens the panel becomes its owner, so register right away.

    1. 1Run sudo -i to become root. The prompt now ends with #. If you logged in as root, nothing changes.
    2. 2Paste the install command below and press Enter. It takes a few minutes.
    3. 3If it stops with This script must be run as root, run sudo -i and try again. If it stops with Error: something is already running on port 80 (or 443, or 3000), other software uses that port: start again with a fresh server.
    4. 4When it prints Congratulations, Dokploy is installed!, wait about 15 seconds. Then open http://SERVER-IP:3000 in your browser.
    5. 5On the Setup the server page, fill in First Name, Last Name, Email, Password (at least 8 characters) and Confirm Password. Click Register.
    6. 6Save the email and password in your password manager.
    Install Dokploy (as root)
    bash
    curl -sSL https://dokploy.com/install.sh | sh

    Check: The message User registered successfully appears and the Dokploy dashboard opens. If a sign-in page opens instead, sign in with the same email and password.

  3. 3

    Give the panel its own HTTPS address

    The panel now runs on plain http with a port number. Give it a domain with HTTPS before you connect GitHub in step 4, because the GitHub connection remembers the address you create it from.

    1. 1At your domain's DNS provider, add an A record. Name: panel (some providers want the full panel.your-domain.com). Value: your server IP. On Cloudflare, keep the record on DNS only (grey cloud) until HTTPS works.
    2. 2Wait until the name points at the server. This can take up to 30 minutes. In PowerShell or Terminal on your computer, run nslookup panel.your-domain.com: when it is ready, the last Address line shows your server IP.
    3. 3In Dokploy, open Settings → Web Server. In the Server Domain card, fill in the fields below and click Save.
    4. 4Open https://panel.your-domain.com and sign in. Use only this address from now on.
    Domain
    panel.your-domain.com
    Let's Encrypt Email
    Your real email address. Always fill this in: Dokploy uses it for every certificate, including your store's.
    HTTPS
    On
    Certificate Provider
    Let's Encrypt
    Optional, and only after the https address works: close the old http://SERVER-IP:3000 address. As root on the server, run docker service update --publish-rm "published=3000,target=3000,mode=host" dokploy. If you do this before HTTPS works, you lock yourself out of the panel.

    Check: https://panel.your-domain.com opens the Dokploy sign-in page with a padlock in the address bar. The first certificate can take a minute; reload the page if the browser warns at first.

  4. 4

    Connect GitHub

    Dokploy needs permission to read your private Storify repository. You give it by creating a small GitHub App from the panel, on the https address from step 3. Path B does not need this step.

    1. 1In Dokploy, open Settings → Git and click Github.
    2. 2Leave GitHub URL as it is. Turn on Organization? only if the repository belongs to a GitHub organization, and type the organization's name.
    3. 3Click Create GitHub App. GitHub opens with a suggested app name. Keep it (the name must be unique) and confirm the creation on GitHub.
    4. 4Back on the Dokploy Git page, the new entry shows Action Required. Click the install icon next to it.
    5. 5On GitHub, choose your account, pick Only select repositories, select your Storify repository, and click Install & Authorize.
    Did you create the GitHub App from http://SERVER-IP:3000? Then GitHub sends its push messages to that address, and automatic deploys stop once port 3000 is closed. Delete the entry on the Git page and create it again from https://panel.your-domain.com.

    Check: The Git page lists your GitHub App without the Action Required label. In step 7, your repository appears in the Repository list.

  5. 5

    Create the project

    A project keeps the store and its database together in one place.

    1. 1Click Projects in the sidebar, then Create Project.
    2. 2Fill in the fields below and click Create.
    3. 3Open the new project. Dokploy adds a production environment inside it automatically; everything you create next goes there.
    Name
    Storify
    Description
    Optional. You can leave it empty.

    Check: The project page shows No services added yet. Click on Create Service.

  6. 6

    Create the MongoDB database

    Storify keeps all store data in MongoDB. Choose one option. A: MongoDB runs inside Dokploy, on this server. B: MongoDB Atlas, a database hosted by MongoDB. The fields below are for option A.

    1. 1Option A: in the project, click Create Service → Database. Select MongoDB, fill in the fields below, and click Create.
    2. 2Open the new database. On its General tab, click Deploy, then Confirm. Wait a minute or two.
    3. 3In the Internal Credentials card, copy the Internal Connection URL. Copy it, do not type it: the host name has a random ending, like storify-mongo-ab12cd.
    4. 4Add the database name: type storify right after 27017/, before the ? (see the example below).
    5. 5Leave External Port (Internet) in the External Credentials card empty. Setting it would open the database to the whole internet.
    6. 6Option B: follow the steps "Create the MongoDB database" and "Build your MongoDB connection string" in Before You Install. In the Atlas IP Access List, add your server's IP address.
    7. 7Keep the final connection string in your notes. You paste it as MONGODB_URI in step 8.
    Name
    mongo
    App Name
    Keep the suggested value, for example storify-mongo. Dokploy adds a random ending.
    Database User
    mongo (the default)
    Database Password
    Leave it empty and Dokploy creates a safe one. If you type your own, use letters and digits only.
    Docker image
    Leave it empty (Dokploy uses mongo:8).
    Use Replica Sets
    Off
    Option A: a finished MONGODB_URI (your password and host name differ)
    txt
    mongodb://mongo:PASSWORD@storify-mongo-ab12cd:27017/storify?authSource=admin&directConnection=true
    Which option? A costs nothing extra and keeps everything on one server, but it uses the server's memory and you set up its backups yourself (step 15). B keeps the database off your server; the free Atlas tier (M0, 512 MB) has no automatic backups.

    Check: You have one connection string saved, and it contains /storify?.

  7. 7

    Create the application and connect the repository

    The application is the store itself. Dokploy downloads your code from GitHub and builds it with Nixpacks, its default builder. The result runs in a container: a sealed package that holds the store and everything it needs.

    1. 1In the project, click Create Service → Application. Name: store. Click Create.
    2. 2Open the new application. On the General tab, go to the Provider card and choose Github.
    3. 3Fill in the fields below and click Save.
    4. 4In the Build Type card, choose Nixpacks (it is usually selected already) and click Save.
    5. 5No GitHub? The Provider card's Drop tab accepts a .zip of the app folder instead (with its hidden files, without node_modules and .next). There are no automatic deploys then: every update is a new zip upload.
    Github Account
    The GitHub App from step 4
    Repository
    Your private Storify repository
    Branch
    main
    Build Path
    /
    Trigger Type
    On Push
    Watch Paths
    Leave empty
    Do not choose Dockerfile or Railpack. Storify has no Dockerfile that builds on the server: the one in deploy/ only packages an image that GitHub Actions has built (Path B), and on its own it stops with no prebuilt .next/. Railpack is not tested with Storify and can leave out .pnpmfile.cjs, which makes the package install fail.

    Check: The Provider card shows your repository and the branch main, and the Build Type card shows Nixpacks.

  8. 8

    Add the environment variables

    Dokploy gives these values to the build and to the running store. NIXPACKS_NODE_VERSION=24 makes the build use Node.js 24, and the two BUILD_ lines help the build fit on a 4 GB server.

    1. 1Open the application's Environment tab.
    2. 2Paste the block below into the Environment Settings box.
    3. 3Replace the MONGODB_URI value with your string from step 6. Replace each <…> with your own 64-character secret, and remove the < and > too.
    4. 4In both URL lines, replace shop.your-domain.com with your store address: exactly what shoppers will type, starting with https://, with no / at the end. Sign-in on any other address fails with 403 Invalid origin.
    5. 5On a server with 8 GB of RAM or more, you may delete the two BUILD_ lines.
    6. 6Click Save.
    7. 7Later changes: after you change a NEXT_PUBLIC_ value, click Deploy, because that value is built into the app (Path B: change it in GitHub instead, as step 17 shows). For other changes, Reload is enough (deleting a variable needs Deploy).
    Environment Settings
    bash
    MONGODB_URI=mongodb://mongo:PASSWORD@storify-mongo-ab12cd:27017/storify?authSource=admin&directConnection=true
    MONGODB_DB_NAME=storify
    BETTER_AUTH_SECRET=<64 characters from the secret command>
    BETTER_AUTH_URL=https://shop.your-domain.com
    NEXT_PUBLIC_APP_URL=https://shop.your-domain.com
    CRON_SECRET=<a different 64 characters>
    NIXPACKS_NODE_VERSION=24
    BUILD_MAX_CPUS=2
    BUILD_TURBOPACK_MEMORY_MB=3072
    Do not paste the whole .env.example file here. Its placeholder values count as real: the STORAGE_* placeholders make the installer skip the storage step, ADMIN_PASSWORD=change-me becomes the password pnpm create-admin gives, and CRON_SECRET=replace-with-… passes as a real secret. Add only the variables you actually fill in.

    Check: After you reload the page, the Environment Settings box still shows your lines, with your own values and no < or > left.

  9. 9

    Point your store domain at the server and add it

    Create the DNS record first and add the domain in Dokploy second. In this order, Traefik gets the HTTPS certificate straight away.

    1. 1At your DNS provider, add an A record. Name: shop (or @ for the bare domain). Value: your server IP. On Cloudflare, keep it on DNS only (grey cloud) until HTTPS works.
    2. 2Wait until nslookup shop.your-domain.com shows your server IP in its last Address line (up to 30 minutes).
    3. 3In the application, open the Domains tab and click Add Domain.
    4. 4Fill in the fields below and click Create.
    Host
    shop.your-domain.com
    Path
    /
    Container Port
    3000
    HTTPS
    On
    Certificate Provider
    Let's Encrypt
    Domain changes work without a new deploy. The dice button next to Host creates a free …sslip.io test address, but it is HTTP only: fine for a quick look, not for the installer when your URL variables start with https://.

    Check: The domain appears in the list, and its Validate DNS button reports DNS Valid.

  10. 10

    Deploy the store

    The first deploy downloads the code, installs the packages and builds the store. As a rough guide, it takes 3–10 minutes, longer on a small server.

    1. 1Open the General tab, click Deploy, then Confirm. Dokploy switches to the Deployments tab.
    2. 2The new row shows running. Click View to follow the build log.
    3. 3The line Nixpacks build completed. means the build worked. The store starts a few seconds later.
    4. 4When the row shows done, open https://shop.your-domain.com.
    5. 5If you see an error page instead of the installer, open https://shop.your-domain.com/install directly. Its System check shows what is wrong.
    With the Dokploy database (option A), the log shows several MongoDB connection error … ENOTFOUND lines and a [sitemap] … emitting hub pages only line. They are expected and harmless: the build cannot see that database, but the running store can. A real failure ends with Nixpacks build failed.

    Check: The row shows done, and https://shop.your-domain.com sends you to /install.

  11. 11

    Run the installer

    Do this now. Until an admin exists, the first person who opens the installer becomes the store owner.

    1. 1Open https://shop.your-domain.com/install (the store sends you there by itself).
    2. 2Complete the five steps as described in First Run & Installer: System check, admin account, store basics, media storage and template.
    3. 3If a System check row shows a yellow warning triangle, fix that value in the Environment tab → Save → Deploy. Wait for done, then click Check again (or reload the page if that button is not shown).
    Do not run pnpm create-admin or pnpm db:seed before the installer. Both create an admin, and from then on /install answers 404 for good.

    Check: The installer shows Your store is ready. Go to sign in opens /login, and after you sign in, /admin shows the dashboard. A "Background jobs have stopped" notice there is expected: you add the jobs in the next step.

  12. 12

    Schedule the 14 background jobs

    Storify has 14 background jobs (cron jobs: tasks that run on a timer), and Dokploy runs none of them by itself. You add them as 8 schedules, one for each command block below. Each command runs inside the store's container, where CRON_SECRET is already set.

    1. 1Open the application's Schedules tab and click Add Schedule.
    2. 2Fill in the fields below. For Command, click Copy on the matching block below and paste it as it is.
    3. 3Click Create Schedule.
    4. 4Repeat for all 8 blocks.
    Task Name
    The name in the block's title, for example storify-every-minute
    Schedule
    The cron expression in the block's title, for example * * * * *. Type it into the box under the preset list (the one that says Custom cron expression).
    Timezone
    Leave empty (UTC)
    Shell Type
    Bash
    Command
    The whole command from that block
    Enabled
    On
    storify-every-minute (* * * * *)
    bash
    node -e "Promise.all(process.argv.slice(1).map(p=>fetch('http://localhost:3000/api/cron/'+p,{headers:{authorization:'Bearer '+process.env.CRON_SECRET}}).then(r=>{console.log(p,r.status);if(!r.ok)process.exitCode=1})))" messaging-outbox carrier-shipments
    storify-every-5-min (*/5 * * * *)
    bash
    node -e "Promise.all(process.argv.slice(1).map(p=>fetch('http://localhost:3000/api/cron/'+p,{headers:{authorization:'Bearer '+process.env.CRON_SECRET}}).then(r=>{console.log(p,r.status);if(!r.ok)process.exitCode=1})))" email-deliveries messaging-escalations boosts
    storify-every-15-min (*/15 * * * *)
    bash
    node -e "Promise.all(process.argv.slice(1).map(p=>fetch('http://localhost:3000/api/cron/'+p,{headers:{authorization:'Bearer '+process.env.CRON_SECRET}}).then(r=>{console.log(p,r.status);if(!r.ok)process.exitCode=1})))" orange-money-reconcile mtn-momo-reconcile abandoned-checkouts
    storify-every-30-min (*/30 * * * *)
    bash
    node -e "Promise.all(process.argv.slice(1).map(p=>fetch('http://localhost:3000/api/cron/'+p,{headers:{authorization:'Bearer '+process.env.CRON_SECRET}}).then(r=>{console.log(p,r.status);if(!r.ok)process.exitCode=1})))" carrier-tracking checkout-expiry
    storify-hourly (0 * * * *)
    bash
    node -e "Promise.all(process.argv.slice(1).map(p=>fetch('http://localhost:3000/api/cron/'+p,{headers:{authorization:'Bearer '+process.env.CRON_SECRET}}).then(r=>{console.log(p,r.status);if(!r.ok)process.exitCode=1})))" vendor-subscriptions
    storify-disputes (40 * * * *)
    bash
    node -e "Promise.all(process.argv.slice(1).map(p=>fetch('http://localhost:3000/api/cron/'+p,{headers:{authorization:'Bearer '+process.env.CRON_SECRET}}).then(r=>{console.log(p,r.status);if(!r.ok)process.exitCode=1})))" gateway-disputes
    storify-finance (0 3 * * *)
    bash
    node -e "Promise.all(process.argv.slice(1).map(p=>fetch('http://localhost:3000/api/cron/'+p,{headers:{authorization:'Bearer '+process.env.CRON_SECRET}}).then(r=>{console.log(p,r.status);if(!r.ok)process.exitCode=1})))" finance
    storify-preorders (0 4 * * *)
    bash
    node -e "Promise.all(process.argv.slice(1).map(p=>fetch('http://localhost:3000/api/cron/'+p,{headers:{authorization:'Bearer '+process.env.CRON_SECRET}}).then(r=>{console.log(p,r.status);if(!r.ok)process.exitCode=1})))" preorders
    A 401 in the run log means CRON_SECRET is missing from the Environment tab, or was changed and not applied yet. Fix it, click Reload, and run the schedule again. A run that gets 401 is not recorded, so the dashboard reports that job as never run.

    Check: The Schedules tab lists 8 schedules. Click the play button (Run Manual Schedule) on storify-every-minute, then the list icon next to it: the run log shows messaging-outbox 200, carrier-shipments 200 and Command executed successfully. After an hour, a new "Background jobs have stopped" notice should name at most vendor-subscriptions, gateway-disputes, finance and preorders; after a day it should not appear at all. If it still names messaging-outbox or carrier-shipments after an hour, the schedules are not running.

  13. 13

    Keep the image cache across deploys (optional)

    Storify saves resized product images in /app/.next/cache. A volume (storage that survives a new deploy) keeps that folder, so the first visitors after an update do not wait while every image is resized again.

    1. 1Open the Advanced tab. In the Volumes card, click Add Volume.
    2. 2Choose Volume Mount, fill in the fields below, and click Create.
    3. 3Open the General tab and click Reload to apply it. No rebuild is needed.
    Volume Name
    storify-next-cache
    Mount Path (In the container)
    /app/.next/cache

    Check: The Volumes card lists the mount path /app/.next/cache.

  14. 14

    Run commands inside the container when you need them

    Some tasks, such as migrations after an update, are commands you run inside the store's container. Dokploy has a terminal for this in the browser.

    1. 1On the General tab, click Open Terminal. In the Docker Terminal window, pick the store's container under Select a container, and keep Bash.
    2. 2The terminal starts in /. Each time you open it, first run cd /app && touch .env. The scripts need a .env file to exist; an empty one is fine, because the Environment tab values still apply. Every deploy makes a new container, so the file does not stay.
    3. 3If a command answers pnpm: command not found, run bash -l, then run the command again.
    4. 4Locked out of the admin? pnpm create-admin you@your-domain.com "NewStrongPassword" resets that user's password. See First Run & Installer.
    First commands in the terminal
    bash
    cd /app && touch .env
    pnpm db:migrate --list
    Never type commands into Advanced → Run Command. That field replaces the store's start command, so the store stops working.

    Check: pnpm db:migrate --list prints Storify's migrations, grouped by release.

  15. 15

    Back up the database

    Set up backups before the store takes real orders. How you do it depends on the database you chose in step 6.

    1. 1Option A (Dokploy MongoDB): create a private bucket just for backups, with its own access key. Cloudflare R2 works; Storage & Media shows how to create a bucket and a key.
    2. 2In Dokploy, open Settings → S3 Destinations → Add Destination. For R2: Name backups, Provider Cloudflare R2 Storage, your Access Key Id and Secret Access Key, Bucket = the backup bucket's name, Region auto, Endpoint https://ACCOUNT-ID.r2.cloudflarestorage.com. Click Test Connection, then Create.
    3. 3Open the MongoDB service → Backups tab → Create Backup. Fill in the fields below and click Create.
    4. 4Click Run Manual Backup on the new row to test it now.
    5. 5Option B (Atlas): the free M0 tier has no automatic backups. Use a paid Atlas tier with backups, or mongodump as shown in "Set up backups" in Launch Checklist.
    6. 6Either way, keep a copy of your Environment tab values in your password manager.
    Destination
    The destination you just added
    Database
    storify
    Schedule
    0 2 * * * (every night at 02:00 UTC)
    Keep the latest
    14 (if empty, every backup is kept)
    Enabled
    On
    Never put backups in the public bucket that serves your product images. A backup holds your customers' data.

    Check: After Run Manual Backup, a new backup file appears in your backup bucket.

  16. 16

    Install a future update

    A new Storify release arrives as a new zip from CodeCanyon. You put the new files into your repository, and Dokploy rebuilds by itself when you push.

    1. 1Read that release's notes in Updates & Migrations, and back up the database first (step 15).
    2. 2Show hidden files (Windows Explorer: View → Show → Hidden items; macOS Finder: Cmd+Shift+.). In your local repository folder, delete everything except the hidden .git folder, then copy in everything from the new release's app folder, hidden files included. Otherwise, files that the new release removed stay behind and can break the build. If you changed Storify's code yourself, apply those changes again. Never copy a .env file into it.
    3. 3Commit and push. In GitHub Desktop: Commit to main, then Push origin. On the command line: the commands below.
    4. 4Dokploy starts a new deployment by itself (the Autodeploy switch on the General tab is on by default). Follow it in the Deployments tab.
    5. 5When it shows done, open the terminal (step 14) and run the migrations the release notes list, for example pnpm db:migrate <name>.
    6. 6Your Environment tab values stay as they are. Add only the new variables the release notes ask for.
    7. 7Path B: the push starts the Deploy image workflow instead. Dokploy redeploys by itself if you added the three DOKPLOY_ secrets (step 18); otherwise click Deploy after the run turns green.
    Command line instead of GitHub Desktop
    bash
    git add .
    git commit -m "Update Storify"
    git push
    Dokploy itself updates separately. As root on the server, run curl -sSL https://dokploy.com/install.sh | sh -s update.

    Check: The Deployments tab shows a new done row, and /admin opens with your products and orders.

  17. 17

    Advanced: build the image in GitHub Actions (Path B)

    Use this instead of step 10 when the server should never build. GitHub builds the store into an image (a ready-to-run package of the store) and stores it in your GitHub account. Dokploy then only downloads and starts it (step 18).

    1. 1Check that your repository contains .github/workflows/deploy-image.yml. It ships with Storify, but a folder whose name starts with a dot is easy to leave out, because Windows and macOS hide it.
    2. 2On GitHub, open your repository → Settings → Secrets and variables → Actions.
    3. 3Variables tab → New repository variable: Name NEXT_PUBLIC_APP_URL, Value https://shop.your-domain.com. It is built into the image, so changing it in Dokploy has no effect; to change it later, edit this variable and run the workflow again. Add any other NEXT_PUBLIC_ values you use here too.
    4. 4Secrets tab → New repository secret: Name BUILD_MONGODB_URI, Value = the same connection string as your MONGODB_URI. The workflow stops if it is missing.
    5. 5The build does not need to reach the database. If it cannot (the Dokploy database, or Atlas without 0.0.0.0/0), the log shows MongoDB connection error lines and the build still finishes; the sitemap fills in once the store runs.
    6. 6Open the Actions tab → Deploy image → Run workflow, and confirm with the green Run workflow button. Pushing a commit to main also starts it.
    7. 7When the run is green, open your GitHub profile → Packages. The image is listed there as a private package, named after your repository.
    If Deploy image is missing from the Actions tab, if GitHub reports Invalid workflow file, or if the run fails before its Build step, stop here and contact Storify support with a screenshot of the run. Do not edit the workflow yourself. You can use Path A (step 10) meanwhile.

    Check: The Actions tab shows a green check next to the Deploy image run, and your Packages page lists the image.

  18. 18

    Advanced: run the prebuilt image in Dokploy (Path B)

    Now let the application download your image instead of building code. The Environment tab, domain, schedules, volume and terminal work exactly as in Path A. When the store runs, continue at step 11.

    1. 1On GitHub, create a token that lets Dokploy download the image: profile picture → Settings → Developer settings → Personal access tokens → Tokens (classic) → Generate new token (classic). Tick only read:packages. Pick an expiration you will remember: when the token expires, deploys fail. Generate it and copy it.
    2. 2In Dokploy, open Settings → Registry → Add Registry. Registry Name ghcr, Username = your GitHub username, Password = the token, Registry URL ghcr.io. Leave Image Prefix empty. Click Test Registry, then Create.
    3. 3In the application's General tab, open the Provider card → Docker. Docker Image: ghcr.io/YOUR-USERNAME/storify:main (your GitHub username and repository name, in lowercase). Click Save.
    4. 4Click Deploy, then Confirm. The log downloads the image instead of building it.
    5. 5To roll back, set Docker Image to an older tag from the package page. Each tag is a short commit ID, for example ghcr.io/YOUR-USERNAME/storify:8a45278 (no sha- in front). Then click Deploy.
    6. 6Automatic redeploys (optional): in Dokploy, open Settings → Profile → API/CLI Keys → Generate New Key. Then add three GitHub secrets: DOKPLOY_URL = https://panel.your-domain.com (no / at the end), DOKPLOY_API_KEY = the key, DOKPLOY_APPLICATION_ID = the ID after /application/ in the application's address in your browser (stop before any ?).
    7. 7Without these three secrets, click Deploy in Dokploy after each green run.
    If the log shows Pulling image failed with denied or unauthorized, check that the token has not expired. Then also fill in Registry URL ghcr.io, Username and Password (the token) in the Docker provider card, click Save, and deploy again.

    Check: The deployment row shows done, its log contains Pulling image completed., and https://shop.your-domain.com opens (on a new store, it sends you to /install).

Bad Gateway after a deploy

Right after a deploy, give the store a minute to start. If the error stays, check the domain's Container Port: it must be 3000 (Domains tab → edit the domain). If the build log stopped suddenly, showed JavaScript heap out of memory, or the whole server became slow, the build ran out of memory: add swap (step 1), keep BUILD_MAX_CPUS=2 and BUILD_TURBOPACK_MEMORY_MB=3072 in the Environment tab, and deploy again. Or move to Path B. More causes: Troubleshooting.

Builds fail with DNS errors on some providers

On some VPS providers, such as Hetzner, builds stop with errors like Could not resolve host: github.com. The fix is to give Docker public DNS servers. First run ls /etc/docker/daemon.json. Only if it answers No such file or directory, run sudo mkdir -p /etc/docker && echo '{"dns": ["1.1.1.1", "8.8.8.8"]}' | sudo tee /etc/docker/daemon.json && sudo systemctl restart docker. If the file already exists, add the dns entry to it instead of replacing it. Restarting Docker briefly restarts every container, including Dokploy.

Type webhook addresses with your real domain

Behind Dokploy's Traefik, a few admin screens build a webhook URL from localhost, for example the Meta webhook URL on the chat channels screen. Always type webhook URLs with your store domain, such as https://shop.your-domain.com/api/webhooks/meta.

Chapter 11 · Deploy to Vercel

Deploy Storify to Vercel, step by step

Vercel runs Storify for you: there is no server to manage, HTTPS is included, and the 14 background jobs run by themselves. You import your private GitHub repository, add seven settings, click Deploy and run the installer. Follow the steps in order. The settings must be in place before the first build, and the installer should run right after it.

Read this first

A live store needs Vercel Pro

Vercel's free Hobby plan is for non-commercial use only, and a shop that takes payments is commercial. Hobby also cannot deploy Storify as it ships: the bundled vercel.json has jobs that run every few minutes, and on Hobby the deployment fails with Hobby accounts are limited to daily cron jobs. This cron expression would run more than once per day. Use the Pro plan ($20 per month per deploying seat) or its free 14-day trial.

  • On Pro, and during the Pro trial, Vercel runs all 14 of Storify's scheduled jobs automatically from vercel.json. You set nothing up. Step 8 shows how to check them.
  • Only for a private test on Hobby: delete the whole "crons" block from vercel.json (the file then contains just {}), commit and push. The store then deploys, but Vercel runs no background jobs: failed emails are never retried, unfinished checkouts never expire, and paid orders are not handed to the carrier automatically. You can call the jobs from an external service instead (Scheduled Jobs). Never take real orders this way.
Compare the other ways to install

Before you start

  • A Vercel account. Step 1 creates a team on the Pro plan or the 14-day Pro trial.
  • Storify in a private GitHub repository, with package.json and vercel.json at the top level. See "Put the source in a private GitHub repository" in Before You Install.
  • A MongoDB Atlas database whose IP Access List contains 0.0.0.0/0, and your finished connection string, which contains .mongodb.net/storify?. See "Create the MongoDB database" and "Build your MongoDB connection string" in Before You Install. Vercel has no fixed IP address, so a narrower entry blocks it.
  • Your two secrets, BETTER_AUTH_SECRET and CRON_SECRET (two different 64-character values), saved in a password manager. See "Generate your secrets" in Before You Install.
  • A storage bucket (Cloudflare R2 or another S3-compatible provider), or a plan to create one right after the installer. See "Create a storage bucket" in Before You Install. Vercel cannot keep uploaded files itself.
  • Node.js 24 and pnpm on your own computer (Set Up Your Computer). Vercel has no command line, so database commands for updates and account recovery run from your computer (step 12).
  1. 1

    Choose a Vercel team on the Pro plan

    Every Vercel project belongs to a team. Create the Storify project in a team on Pro or on the Pro trial, not in your Hobby team.

    1. 1Sign in at vercel.com.
    2. 2Open the team switcher: click the team name at the top left of the dashboard.
    3. 3Already have a Pro team? Select it and go to step 2.
    4. 4To start the free trial: at the bottom of the team switcher choose Create Team, enter a name (for example your store's name) and choose the Pro Trial option. If Pro Trial is not offered, your account has already used its one trial.
    The trial lasts 14 days. To stay on Pro, add a payment method under the team's Settings → Billing before it ends. Without one, the team goes back to Hobby, and a live store must run on Pro.

    Check: The team switcher shows your Pro or Pro Trial team as the selected team. Keep it selected for all the next steps.

  2. 2

    Import your GitHub repository

    Create the project from your private repository. From now on, every push to its main branch builds and publishes a new version of the store. Do not use Vercel Drop (zip upload) instead: it builds before you can add the settings, and it creates a new project every time.

    1. 1In the dashboard, click Add New… → Project.
    2. 2Under Import Git Repository, choose GitHub. If Vercel asks, sign in to GitHub and install Vercel's GitHub app.
    3. 3Your repository is not in the list? Click the link to adjust the GitHub app permissions (the wording may differ slightly). On GitHub, choose Only select repositories, pick your Storify repository and save.
    4. 4Click Import next to your Storify repository.
    5. 5Fill in the configure screen as shown below. Do not click Deploy yet.
    Project Name
    storify, or another short name. It becomes your first store address: https://<project-name>.vercel.app. Write it down.
    Framework Preset
    Next.js. Vercel detects it by itself; leave it.
    Root Directory
    ./ (leave it). Change it only if package.json sits in a subfolder of your repository.
    Build and Output Settings
    Leave every override switch off. A custom install command makes Vercel use an old pnpm, and the install fails.
    Environment Variables
    Fill these in next, in step 3.
    A repository in your personal GitHub account works on every plan. A repository that belongs to a GitHub organization can be imported only by a Pro team, not by a Hobby team.

    Check: The configure screen shows your repository, Framework Preset Next.js, and a Deploy button that you have not clicked yet.

  3. 3

    Add the environment variables before you deploy

    Environment variables are the settings the store reads when it builds and runs. Add all seven now, on the same screen: the build and the running store need them from the first minute. ENABLE_EXPERIMENTAL_COREPACK=1 makes Vercel install with exactly the pnpm version Storify expects (10.24.0). Without it, the install can stop with ERR_PNPM_UNSUPPORTED_ENGINE.

    1. 1On the configure screen, click Environment Variables to open that part of the form.
    2. 2Copy the block below into a text editor such as Notepad and fill in your own values: your Atlas connection string, your two secrets, and your Project Name in both addresses. If Vercel later gives the project a different address, you correct it in step 5.
    3. 3Keep MONGODB_DB_NAME=storify in the list. Without it, a connection string that is missing /storify makes the store save everything in a database called test.
    4. 4Click in the first Key field and paste the whole edited block. Vercel should fill one row per line. If it does not, add each line by hand: the text before = is the Key, and the text after it is the Value.
    5. 5If each row lets you choose a type, choose Secret for MONGODB_URI, BETTER_AUTH_SECRET and CRON_SECRET, and Config for the other four. Vercel never shows a Secret value again, so keep your copies in your password manager.
    Environment variables for Vercel (fill in your own values)
    bash
    MONGODB_URI=mongodb+srv://USER:PASSWORD@cluster0.xxxxx.mongodb.net/storify?retryWrites=true&w=majority
    MONGODB_DB_NAME=storify
    BETTER_AUTH_SECRET=<64 characters from Before You Install>
    BETTER_AUTH_URL=https://<project-name>.vercel.app
    NEXT_PUBLIC_APP_URL=https://<project-name>.vercel.app
    CRON_SECRET=<a different 64 characters>
    ENABLE_EXPERIMENTAL_COREPACK=1
    Do not paste the whole .env.example file. Storify treats its placeholder values as real settings: the STORAGE_* placeholders make the installer skip the storage step, and CRON_SECRET=replace-with-… would become your real job secret. Enter only the seven lines above.

    Check: The form lists seven rows, from MONGODB_URI to ENABLE_EXPERIMENTAL_COREPACK. No value has spaces around = or a leftover < or >. Both addresses start with https:// and have no / at the end.

  4. 4

    Deploy and watch the build

    Vercel now installs the packages, builds the store and publishes it. The first deployment of a new project is always your production (live) deployment. It usually takes several minutes.

    1. 1Click Deploy. The build log opens. You do not need to read every line.
    2. 2A warning about "engines": { "node": ">=22.12.0" } that "will automatically upgrade when a new major Node.js Version is released" is normal. Vercel builds Storify with Node.js 24, so you do not need to set a Node.js version.
    3. 3Lines that start with MongoDB connection error or [sitemap] … emitting hub pages only mean the build could not reach your database. The build still finishes, but the running store will probably fail too. Check MONGODB_URI and the 0.0.0.0/0 entry in the Atlas IP Access List before step 6.
    4. 4Wait until Vercel shows the deployment as Ready.
    5. 5Click Continue to Dashboard, or open the project from your dashboard. The label may differ slightly.
    Build failed with ERR_PNPM_UNSUPPORTED_ENGINE? The ENABLE_EXPERIMENTAL_COREPACK=1 row is missing or misspelled (the value is exactly 1), or an Install Command override is switched on (Settings → Build and Deployment; the label may differ slightly). Fix it under Settings, then redeploy without the build cache as shown in step 5. Build failed with Hobby accounts are limited to daily cron jobs? The project is in a Hobby team: create it again in a Pro team (step 1).

    Check: The deployment's status is Ready.

  5. 5

    Confirm the production address

    The production address is where shoppers find the store until you add your own domain. BETTER_AUTH_URL and NEXT_PUBLIC_APP_URL must match it exactly. Use only this address: preview and per-deployment addresses sit behind a Vercel login, and sign-in fails there with Invalid origin.

    1. 1On the project page, look at the Domains of the production deployment. The layout may differ slightly.
    2. 2Find the .vercel.app address that does not contain -git-. That is your production address. If the plain name was already taken, which is likely for a name like storify, it has an extra part after your project name. That is still the address to use.
    3. 3It matches what you entered in step 3? Go straight to the check below.
    4. 4It is different (Vercel adds a suffix when a name is already taken)? Open Settings → Environment Variables, change BETTER_AUTH_URL and NEXT_PUBLIC_APP_URL to the real address, and save.
    5. 5Then redeploy: open Deployments, click ⋯ next to the latest deployment, choose Redeploy, untick Use existing Build Cache and click Redeploy. NEXT_PUBLIC_APP_URL is built into the app, so it needs a fresh build.
    A Vercel login page on the production address means Deployment Protection covers every deployment. Open Settings → Deployment Protection, set Vercel Authentication to Standard Protection and click Save. Standard Protection is the default for new projects: it keeps the production address public and hides only preview addresses. With the stricter setting, shoppers and payment webhooks are blocked.

    Check: Open the production address in a private (incognito) browser window. It redirects to /install and shows the Storify installer, not a Vercel login page. An error page instead? See the tip in step 6.

  6. 6

    Run the installer

    Do this now, before anyone else finds the address: the first person who completes the installer becomes the store owner. First Run & Installer explains each of its five screens.

    1. 1Open your production address. It redirects to /install.
    2. 2System check: all four rows need a green tick. If one shows a warning, change that value under Settings → Environment Variables, redeploy (step 5), then reload the installer page (the Check again button appears only while one of the first three rows fails). An Application URL warning means your two URL variables do not match the address you opened: go back to step 5.
    3. 3Create your admin account: your name, your email and a password of at least 8 characters. Save the password in your password manager.
    4. 4Store basics and Choose your storefront template: fill them in as First Run & Installer describes. You can change them later.
    5. 5Where media is stored: enter your bucket details if you have them. Otherwise tick Set storage up later and do step 7 right after the install.
    6. 6Click Install. On Your store is ready, click Go to sign in.
    The address shows an error page instead of the installer? The store cannot reach its database, so it cannot redirect you. Open https://<project-name>.vercel.app/install directly: the System check shows which setting is wrong. Usually it is the Atlas IP Access List, or special characters in the database password that are not encoded (see "Build your MongoDB connection string" in Before You Install).

    Check: You can sign in at /login with your new owner account, and /admin opens the dashboard. A notice titled "Background jobs have stopped" is normal on a new store: step 8 explains it.

  7. 7

    Connect storage before anyone uploads

    Vercel cannot keep uploaded files, so every image must go to your bucket. An upload that cannot go straight to the bucket passes through Vercel instead, and Vercel accepts at most 4.5 MB per file.

    1. 1No bucket yet? Create one now: see "Create a storage bucket" in Before You Install. Other providers: Storage & Media.
    2. 2If you ticked Set storage up later, open Admin → Settings → Storage, fill in the form, click Test Connection, then save.
    3. 3Open the bucket's CORS rule. CORS is a bucket setting that lists the websites allowed to upload to it. Cloudflare R2: your bucket → Settings → CORS Policy. AWS S3: your bucket → Permissions → Cross-origin resource sharing (CORS) → Edit.
    4. 4Paste the rule below, or correct the one you added before. List your exact store addresses: https, and no / at the end. If you already know your custom domain (step 9), list it too. Save.
    Bucket CORS rule (replace both addresses)
    json
    [
      {
        "AllowedOrigins": [
          "https://<project-name>.vercel.app",
          "https://www.your-domain.com"
        ],
        "AllowedMethods": ["PUT", "GET", "HEAD"],
        "AllowedHeaders": ["content-type"],
        "ExposeHeaders": ["ETag"],
        "MaxAgeSeconds": 3600
      }
    ]
    Without a matching CORS rule, small uploads still work, but files over 4.5 MB fail with File is too large to upload through the server. The address in the browser's address bar must appear exactly in AllowedOrigins.

    Check: Test Connection passes, and a product image you upload in the admin shows on the product page.

  8. 8

    Check the scheduled jobs

    A cron job is a task the platform runs on a timer. Vercel reads Storify's 14 jobs from vercel.json, runs them on the production deployment (times are in UTC), and sends your CRON_SECRET with every call. There is nothing to set up, only to check. Scheduled Jobs lists what each job does.

    1. 1Open the project → Settings → Cron Jobs. It lists 14 jobs, such as /api/cron/messaging-outbox and /api/cron/finance.
    2. 2Next to /api/cron/messaging-outbox (it runs every minute), click View Logs. After a few minutes, each run shows the status 200.
    3. 3About an hour after the install, open /admin. A new store first shows a notice titled "Background jobs have stopped", because a job that has not had its first turn counts as stopped. After an hour, a new notice should name at most the hourly and daily jobs (vendor-subscriptions, gateway-disputes, finance, preorders). After a day it should not appear at all.
    4. 4If messaging-outbox or carrier-shipments is still named after an hour, the jobs are not running. Check View Logs and the note below.
    5. 5To call one job by hand, use "Test one job by hand" in Scheduled Jobs with your production address.
    Opening a job's address in a browser shows 401 Unauthorized. That is expected: a browser cannot send the secret. A 401 in View Logs means CRON_SECRET is missing, or was changed without a redeploy.

    Check: Settings → Cron Jobs lists 14 jobs, and View Logs for messaging-outbox shows runs with status 200.

  9. 9

    Add your custom domain

    Give the store its own address, such as www.your-domain.com or shop.your-domain.com. Vercel shows the DNS records to add at the company that manages your domain (your registrar or DNS provider), and it sets up HTTPS by itself. More background: Custom Domain & SSL.

    1. 1Open the project → Settings → Domains → Add Domain.
    2. 2Enter your domain and confirm. For a main domain such as your-domain.com, Vercel offers to add www.your-domain.com as well. Accept it.
    3. 3Decide which one is your store address. Vercel recommends www.your-domain.com, with your-domain.com redirecting to it. To change the direction, click Edit on a domain and use Redirect to (the labels may differ slightly).
    4. 4Vercel now shows the records for each domain: an A record for a main domain, and a CNAME record for a subdomain such as www or shop. Copy the exact name and value it shows.
    5. 5At your DNS provider, add those records. Delete any other A, AAAA or CNAME record with the same name.
    6. 6Wait for Vercel to verify them. This often takes a few minutes, but it can take several hours.
    Never copy DNS values from a tutorial or from another project. Vercel gives each project its own values and checks for exactly those. If your DNS is at Cloudflare, set these records to DNS only (grey cloud), so that Vercel can issue the HTTPS certificate.

    Check: Settings → Domains shows each domain as Valid Configuration, with no error, and https://your-domain opens the store with a padlock in the browser's address bar.

  10. 10

    Switch the store to your domain

    The store still believes its address is the vercel.app one. Point both URL variables at your domain and rebuild. Otherwise sign-in on the new domain fails with Invalid origin.

    1. 1Open Settings → Environment Variables.
    2. 2Change BETTER_AUTH_URL and NEXT_PUBLIC_APP_URL to your store address from step 9, for example https://www.your-domain.com. Use your main address, not the one that redirects to it, with https and no / at the end. Save.
    3. 3Open Deployments, click ⋯ next to the latest deployment, choose Redeploy, untick Use existing Build Cache and click Redeploy.
    4. 4When the new deployment is Ready, open https://your-domain/login and sign in.
    5. 5Add the new address to the bucket's CORS rule (step 7) if it is not there yet.
    6. 6Point your payment webhooks and social sign-in redirect addresses at the new domain. The full list is in the Launch Checklist.
    From now on, sign-in works only on this domain. The vercel.app address still shows the shop, but signing in there fails with Invalid origin. That is expected.

    Check: You can sign in at https://your-domain/login, and /admin opens the dashboard.

  11. 11

    Run the server code near your database

    Vercel runs Storify's server code in Washington, D.C. (iad1) unless you choose another region. Every page asks MongoDB for data several times, so the server code should run in the same region as your Atlas cluster. A distant region makes every page slower.

    1. 1In Atlas, open your cluster and note its cloud provider and region, for example AWS Frankfurt (eu-central-1).
    2. 2In Vercel, open the project → Settings → Functions → Function Regions.
    3. 3Select the matching region from the list below, clear any other region, and click Save.
    4. 4Redeploy (Deployments → ⋯ → Redeploy). The new region applies only to new deployments. You can keep the build cache this time.
    Atlas AWS N. Virginia (us-east-1)
    iad1 Washington, D.C. This is the default: nothing to change.
    Atlas AWS Ohio (us-east-2)
    cle1 Cleveland
    Atlas AWS Oregon (us-west-2)
    pdx1 Portland
    Atlas AWS Frankfurt (eu-central-1)
    fra1 Frankfurt
    Atlas AWS Ireland (eu-west-1)
    dub1 Dublin
    Atlas AWS London (eu-west-2)
    lhr1 London
    Atlas AWS Mumbai (ap-south-1)
    bom1 Mumbai
    Atlas AWS Singapore (ap-southeast-1)
    sin1 Singapore
    Atlas AWS Sydney (ap-southeast-2)
    syd1 Sydney
    Atlas AWS São Paulo (sa-east-1)
    gru1 São Paulo
    Is your cluster on Google Cloud or Azure? Choose the Vercel region in the same city, or the nearest one. Storify needs only one region.

    Check: Settings → Functions → Function Regions shows only the region you chose.

  12. 12

    Run maintenance commands from your computer

    Vercel has no command line (shell). When you need a database command, such as an update's migrations or pnpm create-admin to get back into a locked account, run it on your own computer against the live database.

    1. 1Use a copy of the same Storify version that is live, for example the repository folder you push from. Run pnpm install in it once.
    2. 2Your computer can already reach Atlas, because the 0.0.0.0/0 entry allows every address. If you removed that entry, add your current IP address in Atlas.
    3. 3In that folder, create a file named .env with the three lines below. Use the same values as on Vercel, from your password manager: Vercel cannot show Secret values again. Git ignores .env files, so this one is never pushed.
    4. 4Using Windows Notepad? In the Save dialog, set Save as type to All files, so that the file is named .env and not .env.txt.
    5. 5Open a terminal in that folder (Windows 11: right-click inside the folder in File Explorer → Open in Terminal) and run the command you need. The commands below are the same on macOS and Linux. On Windows, pnpm create-admin works in any terminal, but pnpm db:migrate stops in Command Prompt and PowerShell with 'C:\Program' is not recognized as an internal or external command. Run the migrations from WSL2 (Ubuntu) instead: clone your repository inside WSL, install Node.js 24 and pnpm there, and create the same .env in that copy.
    6. 6When you finish, delete this .env or put your local values back, so that you cannot change the live store by accident.
    .env on your computer (it points at the LIVE database)
    bash
    MONGODB_URI=mongodb+srv://USER:PASSWORD@cluster0.xxxxx.mongodb.net/storify?retryWrites=true&w=majority
    MONGODB_DB_NAME=storify
    BETTER_AUTH_SECRET=<the same value as on Vercel>
    Migrations: list them, dry-run them, then apply (read the dry run first)
    bash
    pnpm db:migrate --list
    pnpm db:migrate --all --dry-run
    pnpm db:migrate --all --yes
    Locked out: reset the password of an existing admin
    bash
    pnpm create-admin you@your-domain.com "NewStrongPassword"
    Never run pnpm db:seed, pnpm db:reset or pnpm db:full-reset while this .env points at the live database. They fill your store with demo data or wipe it.

    Check: pnpm db:migrate --all --dry-run reports what each migration would change and writes nothing. If you see .env: not found, the file is missing or has the wrong name.

  13. 13

    Install a future update

    Every new release goes in the same way: new files into your repository, push, let Vercel build, then run the release's database migrations from your computer. First read the notes for every version you are skipping in Updates & Migrations. New scheduled jobs in vercel.json start by themselves.

    1. 1Turn on maintenance mode: Admin → Settings → Maintenance → Enable Maintenance Mode → Save Maintenance Settings.
    2. 2Back up the database: run the mongodump command from "Set up backups" in the Launch Checklist, with your live connection string typed inside the quotes. Before you go on, check that storify-backup.gz exists and is not empty.
    3. 3Show hidden files (see "Download and unzip the package" in Before You Install). In your repository folder, delete everything except the hidden .git folder, which holds your history.
    4. 4Copy in everything from the new release's app folder, hidden files included. If you changed Storify's code yourself, re-apply those changes.
    5. 5Commit and push. In GitHub Desktop: type a summary such as Update Storify, click Commit to main, then click Push origin. Vercel starts a new production build by itself.
    6. 6When the new deployment is Ready, run pnpm install in the folder, then the release's migrations from your computer (step 12): the dry run first, then apply.
    7. 7Check the store, then turn maintenance mode off.
    Command line instead of GitHub Desktop
    bash
    git add .
    git commit -m "Update Storify"
    git push
    Vercel's Instant Rollback puts the previous deployment back in seconds, but it does not undo database migrations, and it does not change the active cron jobs. To go back after migrating, restore your database backup as well.

    Check: The new deployment is Ready, the migrations finish without errors, and the store works as before.

A job shows 504 in View Logs

The job ran past its time limit and Vercel stopped it (FUNCTION_INVOCATION_TIMEOUT). The limit is 60 seconds for finance, messaging-outbox, messaging-escalations, carrier-shipments and carrier-tracking, and 300 seconds for the other jobs. A single 504 is harmless: the job runs again on its next schedule. If it happens every time, check the function region (step 11). On a very large store, a developer can raise maxDuration in app/api/cron/<job>/route.ts (up to 800 seconds on Pro).

Where to find the logs

Build log: Deployments → click a deployment. Errors while the store runs: the project's Logs page (Pro keeps these for one day). Job runs: Settings → Cron Jobs → View Logs. Other errors, such as Invalid origin or File is too large to upload through the server: see Troubleshooting.

After launch: skip the index check on every start

Each time Vercel starts a new copy of the app, Storify re-checks about 370 database indexes, which slows down the first visitors. Once the store works, add MONGODB_AUTO_INDEX=false under Settings → Environment Variables and redeploy. From then on, whenever you update, also run the index migrations that Updates & Migrations lists for stores with MONGODB_AUTO_INDEX=false.

Chapter 12 · Deploy to a VPS

Self-host on a plain Ubuntu server

Run Storify on your own Ubuntu server, without a control panel. Node.js runs the store, systemd (Ubuntu's service manager) keeps it running and starts it again after a reboot, and Caddy (a small web server) adds HTTPS. You type every command yourself over SSH (a command line on the server that you open from your computer). This path gives you full control and asks the most of you.

Read this first

How to use the commands in this chapter

Every command runs on your server, not on your computer. To connect, open PowerShell on Windows 10 or 11 (it includes ssh), or Terminal on macOS and Linux, and type ssh root@SERVER-IP. After step 2 you connect with ssh storify@SERVER-IP instead.

  • Replace the words in capitals (SERVER-IP, YOUR-USERNAME, CHANGE-THIS-PASSWORD, YOUR-CRON-SECRET), anything in <angle brackets>, and shop.your-domain.com with your own values.
  • Paste a whole block at once: Ctrl+V or right-click pastes in PowerShell. If Windows warns that the text contains several lines, click Paste anyway. Lines that start with # are comments; the server ignores them.
  • Before you paste a block that uses sudo, run sudo -v on its own and type the storify password. Nothing appears while you type; that is normal. sudo then remembers it for 15 minutes, so a pasted block does not stop halfway to ask.
  • When a command opens the nano editor: paste the text, press Ctrl+O and then Enter to save, and press Ctrl+X to leave.
  • If a text screen asks which services should be restarted, press Enter to accept.

Before you start

  • A fresh Ubuntu 24.04 or 22.04 server (VPS) with at least 4 GB of RAM, plus its IP address and the root password or SSH key from your hosting provider. Sizes: Requirements.
  • A domain name, and access to its DNS settings (the page where you add records such as A records).
  • Your values from Before You Install: the two secrets, the store address, the storage bucket keys and, if you use Atlas, the MongoDB connection string.
  • Your Storify files: a private GitHub repository (Before You Install), or the app folder (the one with package.json) on your computer.
  • Some comfort with typing commands. If you prefer a web panel, use Deploy with Dokploy instead. Do not install Dokploy on the same server.
  1. 1

    Point your domain at the server

    Do this first, because DNS changes can take a while to reach everyone. Caddy needs this record later to get your HTTPS certificate. The examples use shop.your-domain.com as the store address.

    1. 1Sign in where your domain's DNS is managed: your domain registrar, or Cloudflare.
    2. 2Add an A record. Name: shop (or @ for the bare domain your-domain.com). Value: your server's IP address.
    3. 3Delete any older A, AAAA or CNAME record with the same name. An old AAAA record (IPv6) that points elsewhere stops the certificate.
    4. 4Using Cloudflare? Set the record's proxy status to DNS only (grey cloud) for now. See Custom Domain & SSL.

    Check: nslookup shop.your-domain.com (it works on Windows, macOS and Linux) shows your server's IP address in its last Address line. This can take from a few minutes to a few hours; carry on with the next steps meanwhile.

  2. 2

    Create a user and turn on the firewall

    Log in as root once, create a normal user called storify, and let only SSH (port 22), HTTP (80) and HTTPS (443) through the firewall. Storify never runs as root.

    1. 1Connect as root: ssh root@SERVER-IP. Type yes if SSH asks whether to continue connecting. If your provider gave you a user such as ubuntu instead of root, connect with that user and run sudo -i first.
    2. 2Create the user: adduser storify. Choose a password and save it. Press Enter to skip the other questions.
    3. 3Give the user admin rights: usermod -aG sudo storify.
    4. 4Only if you log in with an SSH key (not a password): copy the key to the new user with rsync --archive --chown=storify:storify ~/.ssh /home/storify. If you connected as ubuntu, write /home/ubuntu/.ssh instead of ~/.ssh.
    5. 5Paste the block below. It opens the three ports, turns the firewall on and switches to the new user.
    6. 6From now on, connect with ssh storify@SERVER-IP.
    bash
    ufw allow OpenSSH
    ufw allow 80,443/tcp
    ufw --force enable
    ufw status
    su - storify
    Some providers also have a firewall in their web panel (for example a cloud firewall or security group). Open ports 22, 80 and 443 there too.

    Check: ufw status shows Status: active with OpenSSH and 80,443/tcp allowed, and the prompt now starts with storify@.

  3. 3

    Add swap memory

    Swap is disk space that Linux uses as extra memory. Building Storify needs much more memory than running it, and without swap a build on a 4 GB server can freeze the whole server. Skip this step if free -h already shows a Swap: line above 0. Run sudo -v first.

    bash
    sudo fallocate -l 4G /swapfile
    sudo chmod 600 /swapfile
    sudo mkswap /swapfile
    sudo swapon /swapfile
    echo '/swapfile none swap sw 0 0' | sudo tee -a /etc/fstab
    free -h

    Check: free -h shows a Swap: line with about 4.0Gi in the total column.

  4. 4

    Install Node.js 24 and pnpm

    nvm (Node Version Manager) installs Node.js for one user only, so run this as storify, not as root. corepack enable then provides pnpm; the exact pnpm version (10.24.0) comes from Storify's package.json.

    bash
    curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.40.8/install.sh | bash
    source ~/.bashrc
    nvm install 24
    nvm alias default 24
    corepack enable
    node -v
    If corepack is "not found" (this happens only on Node.js 25 or newer), run npm install -g corepack and then corepack enable again.

    Check: node -v prints v24. followed by more numbers. You check the pnpm version later, inside the app folder.

  5. 5

    Use MongoDB Atlas (recommended)

    Atlas runs MongoDB for you, so nothing is installed on this server. If you followed Before You Install, your connection string is ready. Check these two things, then skip the next step.

    1. 1In Atlas, open Security → Database & Network Access → IP Access List (the exact labels may differ slightly). Add this server's public IP address (the one in your A record) if it is not there yet.
    2. 2Your connection string contains /storify?, like mongodb+srv://USER:PASSWORD@cluster0.xxxxx.mongodb.net/storify?retryWrites=true&w=majority.
    The Free tier (M0, 512 MB) has no automatic backups. It is fine for testing and a first launch. For a busy live store, use a paid tier with backups (see the step "Back up the database").

    Check: The IP Access List shows your server's IP address, and your string contains .mongodb.net/storify?. The installer's "MongoDB connection" row confirms the connection later.

  6. 6

    Or run MongoDB in Docker on this server

    Skip this step if you use Atlas. Docker runs MongoDB in a container (an isolated package) on this server. Backups and MongoDB upgrades are then your job. Block 1 installs Docker Engine from Docker's own package repository, as described in Docker's guide for Ubuntu.

    1. 1Run sudo -v, then paste block 1. It installs Docker and ends by printing Hello from Docker!.
    2. 2Run mkdir -p ~/mongo && nano ~/mongo/docker-compose.yml. Paste block 2, replace CHANGE-THIS-PASSWORD with a password of letters and digits only, and save.
    3. 3Paste block 3 to start MongoDB.
    4. 4Your connection string is mongodb://storify:CHANGE-THIS-PASSWORD@127.0.0.1:27017/storify?authSource=admin, with your password in it. You need it for .env.
    1. Install Docker Engine
    bash
    sudo apt update
    sudo apt install -y ca-certificates curl
    sudo install -m 0755 -d /etc/apt/keyrings
    sudo curl -fsSL https://download.docker.com/linux/ubuntu/gpg -o /etc/apt/keyrings/docker.asc
    sudo chmod a+r /etc/apt/keyrings/docker.asc
    sudo tee /etc/apt/sources.list.d/docker.sources <<EOF
    Types: deb
    URIs: https://download.docker.com/linux/ubuntu
    Suites: $(. /etc/os-release && echo "$VERSION_CODENAME")
    Components: stable
    Architectures: $(dpkg --print-architecture)
    Signed-By: /etc/apt/keyrings/docker.asc
    EOF
    sudo apt update
    sudo apt install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin
    sudo docker run hello-world
    2. ~/mongo/docker-compose.yml
    yaml
    services:
      mongo:
        image: mongo:7
        container_name: storify-mongo
        restart: unless-stopped
        environment:
          MONGO_INITDB_ROOT_USERNAME: storify
          MONGO_INITDB_ROOT_PASSWORD: CHANGE-THIS-PASSWORD
        volumes:
          - mongodata:/data/db
        ports:
          - "127.0.0.1:27017:27017"
    
    volumes:
      mongodata:
    3. Start MongoDB
    bash
    cd ~/mongo
    sudo docker compose up -d
    sudo docker ps
    Keep the 127.0.0.1: part of the ports line. Ports that Docker publishes bypass the ufw firewall, so without it MongoDB would be open to the whole internet.

    Check: sudo docker ps lists storify-mongo with a STATUS of Up … and PORTS 127.0.0.1:27017->27017/tcp.

  7. 7

    Copy the files with Git (recommended)

    If your Storify code is in a private GitHub repository, let the server download it. A deploy key is an SSH key that can only read this one repository, and it makes later updates a single git pull.

    1. 1Paste block 1. It creates a key and prints one line that starts with ssh-ed25519.
    2. 2On github.com, open your repository → Settings → Deploy keys → Add deploy key.
    3. 3Title: storify-server. Key: paste the whole ssh-ed25519 … line. Leave Allow write access unticked. Click Add key.
    4. 4Paste block 2, with your GitHub user name in place of YOUR-USERNAME (and your repository name in place of storify, if it differs). Type yes when SSH asks whether to continue connecting.
    1. Create a deploy key
    bash
    mkdir -p ~/.ssh && chmod 700 ~/.ssh
    ssh-keygen -t ed25519 -C "storify-server" -f ~/.ssh/id_ed25519 -N ""
    cat ~/.ssh/id_ed25519.pub
    2. Download the code into ~/app
    bash
    git clone git@github.com:YOUR-USERNAME/storify.git ~/app
    ls -a ~/app
    No repository yet? Before You Install shows how to create one with GitHub Desktop. Or use the next step to upload the files instead.

    Check: ls -a ~/app lists package.json, pnpm-lock.yaml, pnpm-workspace.yaml, .pnpmfile.cjs and .env.example.

  8. 8

    Or upload the files from your computer

    Use this if your code is not on GitHub. Upload the contents of the app folder (the one with package.json) to /home/storify/app. Never upload node_modules, .next or .env.

    1. 1Show hidden files first: Windows Explorer → View → Show → Hidden items; macOS Finder → Cmd+Shift+. (full stop). Files such as .pnpmfile.cjs and pnpm-workspace.yaml must be uploaded too.
    2. 2macOS, Linux, or Windows with WSL: open a terminal in the app folder on your computer and run the command below.
    3. 3Windows without WSL: install WinSCP (free). Log in with File protocol SFTP, your server IP as Host name, port 22, user name storify and its password. Press Ctrl+Alt+H in WinSCP to show hidden files.
    4. 4In WinSCP, create the folder /home/storify/app on the server side and open it. Select everything in your app folder except node_modules, .next and .env, and drag it in.
    Run on your computer, inside the app folder
    bash
    rsync -av --exclude .env --exclude node_modules --exclude .next ./ storify@SERVER-IP:/home/storify/app/
    If pnpm-workspace.yaml or .pnpmfile.cjs is missing, the install stops with ERR_PNPM_LOCKFILE_CONFIG_MISMATCH. Upload the missing files and install again.

    Check: On the server, ls -a ~/app lists package.json, pnpm-workspace.yaml, .pnpmfile.cjs and .env.example.

  9. 9

    Write the .env file

    The .env file holds the store's settings and secrets. Write it from the block below, with your production values from Before You Install. Do not copy .env.example as it is: its placeholder values count as real values.

    1. 1Run cd ~/app && nano .env. The editor opens an empty file.
    2. 2Paste the block below. Then change the values as described here.
    3. 3MONGODB_URI: your Atlas string, or the second block if MongoDB runs in Docker on this server.
    4. 4BETTER_AUTH_URL and NEXT_PUBLIC_APP_URL: exactly the address people will open, with https:// and no / at the end.
    5. 5BETTER_AUTH_SECRET and CRON_SECRET: your two secrets. No secrets yet? Run node -e "console.log(require('crypto').randomBytes(32).toString('hex'))" here on the server, once for each. Each run prints a new 64-character value.
    6. 6The two BUILD_ lines keep the build within a 4 GB server's memory. On a server with 8 GB of RAM or more, you may delete them.
    7. 7Save (Ctrl+O, Enter, Ctrl+X). Then run chmod 600 .env so only the storify user can read the file.
    .env
    bash
    MONGODB_URI=mongodb+srv://USER:PASSWORD@cluster0.xxxxx.mongodb.net/storify?retryWrites=true&w=majority
    MONGODB_DB_NAME=storify
    BETTER_AUTH_SECRET=<64 characters from the secret command>
    BETTER_AUTH_URL=https://shop.your-domain.com
    NEXT_PUBLIC_APP_URL=https://shop.your-domain.com
    CRON_SECRET=<a different 64 characters>
    BUILD_MAX_CPUS=2
    BUILD_TURBOPACK_MEMORY_MB=3072
    First line instead, when MongoDB runs in Docker on this server
    bash
    MONGODB_URI=mongodb://storify:CHANGE-THIS-PASSWORD@127.0.0.1:27017/storify?authSource=admin
    Special characters in the MongoDB password break the connection string. Replace them: @ → %40, : → %3A, / → %2F, # → %23, ? → %3F. Or use a password with letters and digits only.

    Check: cat .env shows your lines. No < or > is left, there are no spaces around =, and MONGODB_DB_NAME=storify is there.

  10. 10

    Install and build Storify

    pnpm install downloads the packages and pnpm build compiles the store. The build takes several minutes. Run cd ~/app && pnpm -v on its own first: the first time, Corepack asks ? Do you want to continue? [Y/n] before it downloads pnpm 10.24.0. Press Enter, check that it prints 10.24.0, then paste the block.

    bash
    cd ~/app
    pnpm -v
    pnpm install --frozen-lockfile
    pnpm build
    Lines such as MongoDB connection error … ENOTFOUND or [sitemap] … emitting hub pages only mean the build could not reach the database. The build still finishes and these lines are harmless, but check MONGODB_URI before you start the store. If the build stops with JavaScript heap out of memory, check that swap is on (free -h) and build again.

    Check: pnpm -v prints 10.24.0. The build prints ✓ Compiled successfully and ends with a long table of routes that starts with Route (app). A failed build ends with ELIFECYCLE Command failed instead.

  11. 11

    Run Storify as a service

    systemd starts Storify when the server boots and restarts it if it crashes. Block 1 writes the service file and fills in the path to your Node.js version by itself, so there is nothing to edit.

    1. 1Run sudo -v, then paste block 1 (as the storify user). Its last line prints the start command it wrote.
    2. 2Paste block 2 to start the service and test it.
    1. Write /etc/systemd/system/storify.service
    bash
    NODE_BIN=$(dirname "$(which node)")
    sudo tee /etc/systemd/system/storify.service > /dev/null <<EOF
    [Unit]
    Description=Storify
    After=network.target
    
    [Service]
    Type=simple
    User=storify
    WorkingDirectory=/home/storify/app
    Environment=NODE_ENV=production
    Environment=PATH=$NODE_BIN:/usr/local/bin:/usr/bin:/bin
    ExecStart=$NODE_BIN/pnpm start -H 127.0.0.1 -p 3000
    Restart=always
    RestartSec=5
    
    [Install]
    WantedBy=multi-user.target
    EOF
    grep ExecStart /etc/systemd/system/storify.service
    2. Start and test
    bash
    sudo systemctl daemon-reload
    sudo systemctl enable --now storify
    sleep 10
    systemctl status storify --no-pager
    curl -I http://127.0.0.1:3000
    status=127 or node: No such file or directory in sudo journalctl -u storify -n 50 --no-pager means the Node.js path is wrong: run block 1 again as storify, then sudo systemctl daemon-reload && sudo systemctl restart storify. Do the same if you later install another Node.js version with nvm. No 307? The store probably cannot reach MongoDB: check MONGODB_URI and the Atlas IP Access List, then sudo systemctl restart storify.

    Check: Block 1 prints a line like ExecStart=/home/storify/.nvm/versions/node/v24.…/bin/pnpm start -H 127.0.0.1 -p 3000. systemctl status shows active (running), and curl -I prints HTTP/1.1 307 Temporary Redirect with a location line that ends in /install.

  12. 12

    Install Caddy for HTTPS

    Caddy receives visitors on ports 80 and 443, gets a free Let's Encrypt certificate for your domain by itself, renews it, and passes each request on to Storify. It needs the A record from the first step.

    1. 1Run sudo -v, then paste block 1. These are the commands from Caddy's install guide.
    2. 2In block 2, change shop.your-domain.com to your store's domain, then paste it. It replaces Caddy's example configuration.
    3. 3Paste block 3 to load the new configuration.
    1. Install Caddy
    bash
    sudo apt install -y debian-keyring debian-archive-keyring apt-transport-https curl
    curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/gpg.key' | sudo gpg --dearmor -o /usr/share/keyrings/caddy-stable-archive-keyring.gpg
    curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/debian.deb.txt' | sudo tee /etc/apt/sources.list.d/caddy-stable.list
    sudo chmod o+r /usr/share/keyrings/caddy-stable-archive-keyring.gpg
    sudo chmod o+r /etc/apt/sources.list.d/caddy-stable.list
    sudo apt update
    sudo apt install -y caddy
    2. Write /etc/caddy/Caddyfile
    bash
    sudo tee /etc/caddy/Caddyfile > /dev/null <<'EOF'
    shop.your-domain.com {
        reverse_proxy 127.0.0.1:3000
    }
    EOF
    3. Load it
    bash
    sudo systemctl reload caddy
    systemctl status caddy --no-pager
    No padlock after a few minutes? Run sudo journalctl -u caddy --no-pager | tail -n 30 to see why. The usual causes: the A record does not point at this server yet, port 80 is closed in a firewall, or Cloudflare's proxy is on.

    Check: https://shop.your-domain.com opens in your browser with a padlock and lands on /install.

  13. 13

    Run the installer

    Open your store and finish the five installer screens right away: the first person who opens the installer becomes the owner. Every screen is explained in First Run & Installer.

    1. 1Open https://shop.your-domain.com. It redirects to /install.
    2. 2System check: every row should be green. An Application URL warning means the address in .env differs from the one in your browser. Correct both URL lines in ~/app/.env, run cd ~/app && pnpm build && sudo systemctl restart storify, then reload the installer page.
    3. 3Create your admin account. It becomes the protected Owner.
    4. 4Store basics: store name, default language, currency, and whether this is a multi-vendor marketplace.
    5. 5Where media is stored: enter your bucket details, or tick "Set storage up later" (nothing can be uploaded until you finish it in Admin → Settings → Storage). Buckets, CORS and self-hosted MinIO: Storage & Media.
    6. 6Choose your storefront template and click Install. Then click Go to sign in.
    Do not run pnpm create-admin or pnpm db:seed before this. Both create an admin, and from then on /install answers 404 for good.

    Check: The installer says "Your store is ready". After you sign in at /login, /admin opens the dashboard.

  14. 14

    Schedule the 14 background jobs

    A cron job is a task the server runs on a timer. Storify needs 14 of them: they retry failed emails, hand orders to carriers, expire unfinished checkouts and more (Scheduled Jobs). On a VPS you add them to the storify user's crontab (its list of timed commands).

    1. 1Run grep CRON_SECRET ~/app/.env and copy the value after =.
    2. 2Run crontab -e. If it asks you to choose an editor, type 1 (nano) and press Enter.
    3. 3Paste the block below at the end of the file. Put your value in place of YOUR-CRON-SECRET and your store address in the BASE= line. Save.
    4. 4Test one job now: curl -H "Authorization: Bearer YOUR-CRON-SECRET" https://shop.your-domain.com/api/cron/email-deliveries. The answer starts with {"success":true.
    crontab -e
    bash
    CRON_SECRET=YOUR-CRON-SECRET
    BASE=https://shop.your-domain.com
    * * * * *    curl -fsS -H "Authorization: Bearer $CRON_SECRET" $BASE/api/cron/messaging-outbox > /dev/null
    * * * * *    curl -fsS -H "Authorization: Bearer $CRON_SECRET" $BASE/api/cron/carrier-shipments > /dev/null
    */5 * * * *  curl -fsS -H "Authorization: Bearer $CRON_SECRET" $BASE/api/cron/email-deliveries > /dev/null
    */5 * * * *  curl -fsS -H "Authorization: Bearer $CRON_SECRET" $BASE/api/cron/messaging-escalations > /dev/null
    */5 * * * *  curl -fsS -H "Authorization: Bearer $CRON_SECRET" $BASE/api/cron/boosts > /dev/null
    */15 * * * * curl -fsS -H "Authorization: Bearer $CRON_SECRET" $BASE/api/cron/orange-money-reconcile > /dev/null
    */15 * * * * curl -fsS -H "Authorization: Bearer $CRON_SECRET" $BASE/api/cron/mtn-momo-reconcile > /dev/null
    */15 * * * * curl -fsS -H "Authorization: Bearer $CRON_SECRET" $BASE/api/cron/abandoned-checkouts > /dev/null
    */30 * * * * curl -fsS -H "Authorization: Bearer $CRON_SECRET" $BASE/api/cron/carrier-tracking > /dev/null
    */30 * * * * curl -fsS -H "Authorization: Bearer $CRON_SECRET" $BASE/api/cron/checkout-expiry > /dev/null
    0 * * * *    curl -fsS -H "Authorization: Bearer $CRON_SECRET" $BASE/api/cron/vendor-subscriptions > /dev/null
    40 * * * *   curl -fsS -H "Authorization: Bearer $CRON_SECRET" $BASE/api/cron/gateway-disputes > /dev/null
    0 3 * * *    curl -fsS -H "Authorization: Bearer $CRON_SECRET" $BASE/api/cron/finance > /dev/null
    0 4 * * *    curl -fsS -H "Authorization: Bearer $CRON_SECRET" $BASE/api/cron/preorders > /dev/null
    Your first visit to the dashboard shows a "Background jobs have stopped" notice: the jobs had not run yet, so this is expected. After an hour, a new notice should name at most vendor-subscriptions, gateway-disputes, finance and preorders; after a day, none. If messaging-outbox or carrier-shipments are still named after an hour, the crontab is not running. Details: Scheduled Jobs.

    Check: crontab -l lists the two settings lines and all 14 job lines.

  15. 15

    Back up the database

    Set this up before real orders arrive. With MongoDB in Docker, block 1 makes a backup now and block 2 makes one every night. With Atlas, the Free tier has no backups: for a live store, use a paid tier and turn on its backups.

    1. 1Docker: in block 1, put your MongoDB password in place of CHANGE-THIS-PASSWORD, run sudo -v, then paste it. It writes a file such as ~/backups/storify-2026-01-31.gz.
    2. 2For the nightly backup, run sudo crontab -e (the root user's crontab) and paste block 2 at the end, with your password in it. It backs up at 02:00 and deletes backups older than 14 days.
    3. 3Now and then, download a backup file to your computer (with WinSCP, or by running scp storify@SERVER-IP:backups/FILE-NAME . on your computer). A backup that only lives on the server is lost with the server.
    1. Back up now (MongoDB in Docker)
    bash
    mkdir -p ~/backups
    sudo docker exec storify-mongo mongodump --uri="mongodb://storify:CHANGE-THIS-PASSWORD@127.0.0.1:27017/storify?authSource=admin" --archive --gzip > ~/backups/storify-$(date +%F).gz
    ls -lh ~/backups
    2. sudo crontab -e (every night)
    bash
    0 2 * * *  docker exec storify-mongo mongodump --uri="mongodb://storify:CHANGE-THIS-PASSWORD@127.0.0.1:27017/storify?authSource=admin" --archive --gzip > /home/storify/backups/storify-$(date +\%F).gz
    30 2 * * * find /home/storify/backups -name 'storify-*.gz' -mtime +14 -delete
    To restore a backup (this replaces the current data): sudo docker exec -i storify-mongo mongorestore --uri="mongodb://storify:CHANGE-THIS-PASSWORD@127.0.0.1:27017/?authSource=admin" --archive --gzip --drop < ~/backups/FILE-NAME.gz

    Check: ls -lh ~/backups lists a storify-….gz file larger than 0 bytes. The next morning there is a new one.

  16. 16

    Update to a new release later

    Read Updates & Migrations for every release you skip. Back up the database first. Then bring in the new files, build, restart and run that release's migrations. The store may show errors while the build runs, so choose a quiet time.

    1. 1With Git: on your computer, show hidden files, delete everything in your repository folder except the hidden .git folder, copy in everything from the new release's app folder (hidden files included), apply your own code changes again if you made any, then commit and push. On the server, run cd ~/app && git pull.
    2. 2Without Git: upload the new files the same way as before. With rsync, add --delete after -av, so files that the new release removed are removed on the server too. Your .env is never uploaded, so the server keeps its settings.
    3. 3Run sudo -v, then paste block 1 to install, build and restart.
    4. 4Run only the migrations that Updates & Migrations lists for the releases you skipped (block 2).
    1. Install, build and restart
    bash
    cd ~/app
    pnpm install --frozen-lockfile
    pnpm build
    sudo systemctl restart storify
    2. Migrate
    bash
    cd ~/app
    pnpm db:migrate --list
    pnpm db:migrate MIGRATION-NAME --dry-run
    pnpm db:migrate MIGRATION-NAME

    Check: systemctl status storify --no-pager shows active (running), and the store opens normally.

Where to look when something goes wrong

Storify: sudo journalctl -u storify -n 100 --no-pager (use sudo journalctl -u storify -f to follow it live; Ctrl+C stops). Caddy and HTTPS: sudo journalctl -u caddy -n 100 --no-pager. MongoDB in Docker: sudo docker logs storify-mongo --tail 100. Known errors and fixes: Troubleshooting.

Chapter 13 · First Run & Installer

Run the installer (five steps)

The first time you open a new Storify store, every page sends you to the installer at /install. In five screens it checks your setup, creates your admin account, saves the store basics, connects media storage and publishes a storefront template. Then it locks itself. This chapter is the same for every path: your computer, Dokploy, Vercel or a VPS.

Runs only once

Run the installer yourself, right after you deploy

Whoever completes the installer first creates the admin account and becomes the store's Owner. So open /install as soon as your store is online, before anyone else finds it.

  • When it finishes, /install answers 404 for good. That is the lock working, not an error.
  • An admin made any other way (pnpm create-admin, pnpm db:seed or pnpm db:seed:users) also closes it. Use the installer or a command, not both.
  • Lost your password later? See "Locked out, or /install shows 404" near the end of this chapter.

Before you start

  • Your store is running and its address opens in a browser: http://localhost:3000 on your computer (Running Locally), or your Dokploy, Vercel or VPS address.
  • Your environment values are set in .env or in your hosting panel, as your path chapter shows. Above all: MONGODB_URI, MONGODB_DB_NAME=storify, BETTER_AUTH_SECRET, and both URL values set to the exact address you will open.
  • Recommended: your storage bucket, its keys and its CORS rule ("Create a storage bucket" in Before You Install, and Storage & Media). No bucket yet? You can tick "Set storage up later" and add it after the install.

How to fix a failed check on each platform

PlatformWhere you change the valueWhat to do after
Your computerThe .env file in the app folder.Stop pnpm dev with Ctrl+C and run it again. If you use pnpm start, run pnpm build first when a NEXT_PUBLIC_ value changed.
VercelYour project → Settings → Environment Variables.Deployments → ⋯ next to the latest deployment → Redeploy. Untick "Use existing Build Cache" when a NEXT_PUBLIC_ value changed.
DokployThe application's Environment tab, then Save.Click Deploy on the General tab. Reload is enough when no NEXT_PUBLIC_ value changed.
Dokploy Path B (prebuilt image)NEXT_PUBLIC_APP_URL: GitHub → your repository → Settings → Secrets and variables → Actions → Variables. Other values: the application's Environment tab, then Save.After a GitHub change: Actions → Deploy image → Run workflow, wait for the green check, then click Deploy in Dokploy. After a Dokploy change: click Deploy.
VPSThe .env file in the app folder on the server.Run pnpm build (only when a NEXT_PUBLIC_ value changed), then sudo systemctl restart storify.

Then go back to the installer and click Check again, or reload the page. NEXT_PUBLIC_ values are fixed when the app is built, so changing one needs a new build, not only a restart.

  1. 1

    Open your store's address

    Open the address people will use for your store. Until the install is done, every page sends you to /install.

    1. 1Your computer: http://localhost:3000.
    2. 2Vercel: your production address, the one in NEXT_PUBLIC_APP_URL, for example https://<project-name>.vercel.app. Do not use a preview or per-deployment address (the longer .vercel.app addresses with random letters): sign-in fails there with "Invalid origin".
    3. 3Dokploy or VPS: your store domain, for example https://shop.your-domain.com.
    Not sent to /install? An error page, or a page that loads for a long time and then fails, means the store cannot reach its database. Pages do not redirect then. Add /install to the address yourself to see which check fails. If the storefront opens, or /install shows 404, the store already has an admin: see "Locked out, or /install shows 404" below.

    Check: The address ends in /install, and the page says "Set up your store" with "System check" below it.

  2. 2

    Pass the system check

    The first screen, "System check", tests four things. The first three block the install until they pass. Application URL is only a warning, but fix it before you continue.

    1. 1Node.js (the row shows your version): needs 22.12 or newer. On your computer or a VPS, install Node.js 24 LTS. On Dokploy, set NIXPACKS_NODE_VERSION=24. Vercel uses Node.js 24 by itself.
    2. 2MongoDB connection: check MONGODB_URI and MONGODB_DB_NAME=storify. Special characters in the database password must be encoded (for example @ as %40). The database must also accept connections from this server: see the Atlas IP Access List in "Create the MongoDB database" in Before You Install. While this row fails, the other rows say "Not checked — the installer could not reach its database".
    3. 3Authentication secret: BETTER_AUTH_SECRET must exist, have at least 32 characters, and not be an example value. Make a new one with the command below, and use exactly the name BETTER_AUTH_SECRET.
    4. 4Application URL: NEXT_PUBLIC_APP_URL and BETTER_AUTH_URL must both equal the address in your browser, with the same http or https and no / at the end. If they differ, signing in fails with "Invalid origin".
    5. 5A green tick means the row passed. A yellow warning triangle means it needs a fix.
    6. 6Change the value as the table "How to fix a failed check on each platform" (above the steps) shows. Then click Check again. That button appears only while a blocking row fails; otherwise reload the page.
    Make a new secret (Windows, macOS and Linux)
    bash
    node -e "console.log(require('crypto').randomBytes(32).toString('hex'))"
    The message under the rows says "restart the app after editing .env". On Vercel and Dokploy there is no .env file: change the value in the hosting panel instead, as the table shows.

    Check: All four rows show a green tick, and Continue is enabled.

  3. 3

    Create your admin account

    This account becomes the store's Owner. The Team page protects it: no other administrator can demote, suspend or remove it.

    Your name
    Your full name.
    Email
    An address you can read. Password-reset emails go there once Email / SMTP is set up.
    Password
    At least 8 characters; the rule is shown under the field. Save it in a password manager.

    Check: Continue becomes clickable once the name, a valid email and a password of 8 or more characters are filled in.

  4. 4

    Fill in the store basics

    These settings shape your shop. You can change every one of them later in Admin → Settings.

    Store name
    Required. The name your customers see.
    Default language
    The language your store opens in. English by default; search the list for another.
    Currency
    USD by default; search the list for your currency.
    Multi-vendor marketplace
    Ticked by default: other sellers can open their own stores. Untick it if only you will sell.
    Point of sale
    Off by default. Tick it to sell in person at a counter.

    Check: Continue becomes clickable once the store name is filled in.

  5. 5

    Connect your media storage (or skip it)

    The screen "Where media is stored" connects the bucket that holds product images, videos and downloads. Where to find each value: Storage & Media.

    1. 1Click your provider: Cloudflare R2 (selected by default), AWS S3, MinIO or DigitalOcean Spaces. The form then shows only that provider's fields (listed below).
    2. 2Fill in the fields and click Test connection. It uploads a small test file and reads it back through the Public URL.
    3. 3Does the test say that no Public URL is set, or that the public URL is not accessible? Make the bucket publicly readable (for R2: turn on the r2.dev subdomain or connect your own domain). Paste that address into Public URL and test again.
    4. 4No bucket yet? Tick "Set storage up later". The install still finishes, but uploads stay unavailable until you add the bucket in Admin → Settings → Storage.
    5. 5Click Continue.
    Account ID
    Cloudflare R2 only. Your 32-character Cloudflare account ID.
    Region
    AWS S3, for example us-east-1. Optional for MinIO.
    Datacenter region
    DigitalOcean Spaces only, for example nyc3.
    Endpoint URL
    MinIO only, for example https://minio.example.com.
    Bucket name
    Every provider.
    Access key ID
    Every provider.
    Secret access key
    Every provider.
    Public URL
    Optional on this screen, but without it images do not show in browsers. For R2: the https://pub-….r2.dev address or your own domain.
    Is "Set storage up later" already ticked, with the message "Storage credentials were found in your .env file"? Then STORAGE_* values are set in your environment, and the store uses them. The placeholders from .env.example also cause this. If they are not your real keys, or you use a provider other than Cloudflare R2, untick the box and fill in the form: values saved here take priority.

    Check: Next to Test connection you see a green message such as "Cloudflare R2 connected and public URL verified".

  6. 6

    Choose your storefront template

    Pick the design your storefront starts with. You can switch at any time later in Admin → Online Store → Themes, without losing products, orders or settings.

    1. 1Click a template card: Electronics (selected by default), Fashion or Classic Marketplace.
    2. 2Leave "Import this … template's demo store data" ticked to start with that template's sample catalog, collections and storefront. It creates no demo logins and no orders.
    3. 3Untick it to start with an empty store.
    The demo products' photos are not copied to your bucket: they load from Storify's public demo storage. Use the demo data to try Storify. For a real store, untick the box, or replace the sample products before launch.

    Check: The card you picked has a highlighted border, and the checkbox names the same template.

  7. 7

    Click Install, then sign in

    Install creates everything in one go and then locks the installer. With demo data it can take a little while.

    1. 1Click Install and wait. Do not close the page.
    2. 2"Your store is ready" appears. Lines with a yellow warning sign under it are warnings, for example "The sample catalog could not be imported — you can import one from Products after signing in". The store is installed anyway; do what the warning says after you sign in.
    3. 3A red message instead, such as "Installation failed — please try again", means the install did not happen. Click Back to fix the screen the message points to, then click Install again. If the message is "Not found not found", the store is already installed: open /login.
    4. 4Click Go to sign in. The sign-in page opens at /login. Sign in with the email and password from the admin screen.
    Your first dashboard visit brings a notification titled "Background jobs have stopped". On a new store this is expected: no job has had its first run yet. Your path chapter shows how to schedule the jobs; on Vercel they run by themselves. On your own computer nothing runs them, so the notice stays, which is fine for a test store. On a server, the frequent jobs drop off the list within about an hour. finance and preorders run once a day, so they can stay on it for up to 24 hours. Scheduled Jobs explains each job.

    Check: You land on the admin dashboard, /admin opens it from now on, and /install now answers 404 (page not found).

  8. 8

    Find your way around

    Add these paths to your store address, for example https://shop.your-domain.com/admin. Your default language has no prefix in the address. Other languages you turn on get one, for example /bn/products.

    Storefront
    /
    Sign in (every role)
    /login
    Admin panel
    /admin (opens the dashboard)
    Staff dashboard
    /staff
    Vendor dashboard
    /vendor/dashboard (only while Multi-vendor marketplace is on)
  9. 9

    Instead of the installer (developers)

    These commands are for development and automated tests. Each one creates an admin, so the installer closes for good and its store, storage and template screens never run. Use the installer or these commands, not both.

    1. 1pnpm db:seed: a full demo store with catalog, orders, a published storefront and demo logins such as admin@storify.com / Admin@123. Run it on an empty database, before anyone opens the site. Other templates and error messages: "Optional: load the demo store instead of the installer" in Database Setup.
    2. 2pnpm db:seed:users: only the four demo logins: admin@storify.com, vendor@storify.com, staff@storify.com and customer@storify.com.
    3. 3pnpm create-admin: only your own admin, with no catalog and no template. Set those up in the admin afterwards. Always type the password: without it, the command uses ADMIN_PASSWORD from .env, which is change-me in .env.example.
    Your own admin only
    bash
    pnpm create-admin you@shop.com "StrongPassword" "Your Name"
    Full demo store
    bash
    pnpm db:seed
    Demo logins only
    bash
    pnpm db:seed:users
    The demo passwords are public. Change every one before anyone else can reach the store. Never run pnpm db:seed, pnpm db:reset or pnpm db:full-reset against a live store's database.

    Check: The command ends without an error. pnpm create-admin prints Successfully created and upgraded … to Admin., and pnpm db:seed ends with a Demo Credentials: list.

  10. 10

    Locked out, or /install shows 404

    /install answers 404 once the store has an admin. That is the lock, not a fault: sign in at /login instead. Forgot the password? Click "Forgot password?" on the sign-in page (needs Email / SMTP), or reset it with pnpm create-admin. The command sets the new password, re-activates the account, makes it an admin and signs it out everywhere.

    1. 1Your computer, or a VPS after you connect with SSH: run the command in a terminal in the app folder (the one with .env). On your computer, pnpm dev can keep running.
    2. 2Dokploy: open the application → General → Open Terminal → pick the container → Bash. Run the two lines from the Dokploy box below. If it says pnpm: command not found, type bash -l, press Enter and run them again.
    3. 3Vercel has no terminal: run the command from your computer, as "Run maintenance commands from your computer" in Deploy to Vercel shows.
    4. 4Sign in at /login with the new password.
    5. 5Only on a test database you can throw away: to see the installer again, run pnpm db:reset in the app folder on your computer. It deletes every product, order, account and setting in the database named in .env, without asking, so first check that .env points at the test database. Then stop pnpm dev with Ctrl+C, start it again and open /install. Or put a new, empty database name in .env instead (in both MONGODB_URI and MONGODB_DB_NAME) and restart.
    Your computer or a VPS
    bash
    pnpm create-admin you@shop.com "NewStrongPassword"
    Dokploy terminal
    bash
    cd /app && touch .env
    pnpm create-admin you@shop.com "NewStrongPassword"
    Always type the password in the command. Without it, the command uses ADMIN_PASSWORD from your environment, which is change-me if that line came from .env.example. Use a long password of letters and digits: characters such as $, !, quotes or spaces can change on the way through the terminal. Change it after you sign in.

    Check: The terminal prints Successfully upgraded … to Admin. and then Password updated.

Upgraded stores never see the installer

A store you upgrade already has an admin, so it opens as usual; see Updates & Migrations. Keep at least one admin account: on a store that was upgraded or set up from the command line, the installer opens again, for anyone, if no admin is left.

Chapter 14 · Launch Checklist

Before you open the store to customers

The installer is done and your store is running. Work through this list once, in order, before you share the address with anyone. Every step ends with a check, so you know it worked. Only trying Storify on your own computer? Do the steps for sign-in, storage and email, use http://localhost:3000 wherever this chapter says https://your-domain, and skip the rest.

Before you start

  • The installer finished and showed "Your store is ready" (see First Run & Installer).
  • You know the final address of your store, for example https://shop.your-domain.com. This chapter writes it as https://your-domain.
  • You can open your hosting panel (Vercel or Dokploy) or sign in to your server.
  • A password manager, or another safe place, for the keys and passwords you create.
  1. 1

    Sign in as the owner

    Sign in with the email and password you entered in the installer. This account is the store Owner: no other administrator can demote, suspend or remove it.

    1. 1Open https://your-domain/login.
    2. 2Enter the admin email and password from the installer and sign in. You land on the admin dashboard. Later you can always reach it at https://your-domain/admin.
    3. 3In the admin menu, open Team. Your account is in the list with the label Owner.
    4. 4Password not accepted? Click Forgot password? (this needs email, so it may not work yet), or follow "Locked out, or /install shows 404" in First Run & Installer.
    Your first visit to the dashboard creates a notification titled "Background jobs have stopped". On a new store this is expected: every job counts as not running until its first run. The step "Check that the background jobs run" below shows how to confirm that they start.

    Check: The dashboard opens at /admin/dashboard, and Admin → Team shows your account marked Owner.

  2. 2

    Confirm the final address and HTTPS

    Sign-in, emails, payment providers and search engines all use one address: the one in your two URL variables. Make it final now, before you set up anything that uses it.

    1. 1Is the store still on a temporary address, such as https://<project-name>.vercel.app or a Dokploy sslip.io test address? Move it to your own domain first with Custom Domain & SSL.
    2. 2Check that BETTER_AUTH_URL and NEXT_PUBLIC_APP_URL are both exactly https://your-domain (https, no / at the end). Vercel: Settings → Environment Variables. Dokploy: the Environment tab. VPS: the .env file.
    3. 3If you changed either value, rebuild, because NEXT_PUBLIC_APP_URL is fixed into the app when it is built. Vercel: Deployments → ⋯ → Redeploy, with "Use existing Build Cache" unticked. Dokploy: Environment tab → Save → Deploy. Dokploy Path B (prebuilt image): also change the NEXT_PUBLIC_APP_URL repository variable on GitHub, run Actions → Deploy image → Run workflow, wait for the green check, then click Deploy in Dokploy. VPS: run pnpm build, then sudo systemctl restart storify.
    4. 4Open https://your-domain/robots.txt in your browser. It shows the address the app was built with.
    5. 5Sign out, then sign in again at https://your-domain/login.
    Open and sign in to the store only on the address in these variables. On any other address (an old domain, a *.vercel.app preview link, the server's IP address) sign-in fails with "Invalid origin".

    Check: The browser shows a padlock on https://your-domain, robots.txt contains the line Sitemap: https://your-domain/sitemap.xml, and you can sign in there.

  3. 3

    Test the storage connection

    Product images, logos and downloads are kept in your storage bucket, not on the server. Test the connection now, before you add products.

    1. 1Open Admin → Settings → Storage (https://your-domain/admin/settings/storage).
    2. 2If you ticked "Set storage up later" in the installer, choose your provider and fill in the bucket details now. Storage & Media shows where to find each value.
    3. 3Click Test Connection, then save. The test uploads a small file and reads it back through the Public URL.
    4. 4Open your bucket's CORS rule (you added it in Before You Install, step "Create a storage bucket"). It must list https://your-domain exactly. CORS is the bucket setting that lets your website send files straight to the bucket.
    5. 5Open a product in Admin → Products (or create one), add an image and save. Then open that product on the storefront.
    Without the right CORS rule, the browser cannot send files straight to the bucket, so they go through your server instead. Large files then fail with "File is too large to upload through the server" (on Vercel, anything over 4.5 MB).

    Check: The test reports "… connected and public URL verified" (for example "Cloudflare R2 connected and public URL verified"), and the new image shows on the storefront.

  4. 4

    Set up email and send a test

    Password resets, team invites and order emails need a working mail server (SMTP). Without it, nobody can reset a forgotten password.

    1. 1Open Admin → Settings → Email Configuration (SMTP) (https://your-domain/admin/settings/email).
    2. 2Turn on Enable SMTP Email.
    3. 3Fill in SMTP Host, SMTP Port, SMTP Username, SMTP Password, From Email and From Name with the values from your email provider. For port 465, also turn on Use SSL/TLS. Email / SMTP lists common providers.
    4. 4Save the settings. The test uses the saved values.
    5. 5Under Test Email, type your own address and click Send Test Email. Then open your inbox, and look in the spam folder too.
    On a VPS or Dokploy server, a test that times out usually means your server provider blocks outgoing mail ports (DigitalOcean, for example, blocks 25, 465 and 587 on new servers). Use port 2525 if your email provider offers it, or ask the provider to unblock SMTP. If the test email lands in spam, set up SPF, DKIM and DMARC for your sender domain. Your email provider's dashboard shows the DNS records to add.

    Check: A message that starts with "Test email sent successfully" appears, and the test email arrives in your inbox.

  5. 5

    Check that the background jobs run

    Storify has 14 background jobs. A background job (cron job) is a task the platform runs on a timer: it retries failed emails, hands paid orders to the carrier, sends abandoned-checkout emails and checks payments. Vercel runs them for you. On Dokploy and a VPS you add them yourself, as shown in Scheduled Jobs.

    1. 1Vercel: open Settings → Cron Jobs. It lists 14 jobs. Click View Logs next to /api/cron/messaging-outbox.
    2. 2Dokploy: open the application's Schedules tab. It lists the 8 schedules from Scheduled Jobs. Click the play button (Run Manual Schedule) on storify-every-minute, then the list icon next to it to see the run.
    3. 3VPS: run crontab -l and check that it lists 14 job lines. Then call one job by hand as shown in Scheduled Jobs, step "Test one job by hand".
    4. 4External scheduler (for example cron-job.org): open the run history of each job.
    5. 5About an hour later, open the admin dashboard again and read the newest "Background jobs have stopped" notification, if there is one.
    A 401 (Unauthorized) means CRON_SECRET is missing or does not match. If messaging-outbox or carrier-shipments are still named after an hour, the schedule is not running: go back to your platform's part of Scheduled Jobs. The dashboard sends the notice only once for the same list of jobs, so check the logs, not only the notice.

    Check: The runs succeed: status 200 in Vercel's logs, messaging-outbox 200 in Dokploy's run log, a reply that starts with {"success":true from the hand test, or 200 in your scheduler's history. After an hour, a new "Background jobs have stopped" notice names at most vendor-subscriptions, gateway-disputes, finance and preorders. After a day it does not appear at all.

  6. 6

    Point webhooks at the live domain

    A webhook is a message a payment or shipping service sends to your store, for example "this payment succeeded". Register only the services you use. Replace your-domain with your store's address.

    Stripe
    https://your-domain/api/payments/webhook. Choose the events listed in Payment Gateways, then paste the signing secret into Admin → Settings → Payment Settings.
    PayPal
    https://your-domain/api/payments/paypal/webhook. Then paste the webhook ID into Payment Settings.
    Paystack
    https://your-domain/api/payments/paystack/webhook
    Razorpay
    https://your-domain/api/payments/razorpay/webhook. Then paste the webhook secret into Payment Settings.
    Pesapal
    Nothing to type. Click Register IPN in Admin → Settings → Payment Settings. It needs NEXT_PUBLIC_APP_URL to be a public https:// address.
    Meta (WhatsApp, Messenger, Instagram)
    https://your-domain/api/webhooks/meta, with the value of META_WEBHOOK_VERIFY_TOKEN as the verify token. See Omnichannel Messaging.
    Telegram
    On Vercel, nothing to type: Storify registers the webhook when you connect the bot (it needs https://). On Dokploy or a VPS, the current release may register the server's internal address (https://localhost:3000/… or https://127.0.0.1:3000/…) instead of your domain, and Telegram refuses that, so connecting the bot can fail there. Contact support before you rely on Telegram on those hosts.
    Shippo
    Click Generate webhook URL in Admin → Settings → Shipping & Delivery, then paste that URL into Shippo.
    Shiprocket
    https://your-domain/api/webhooks/carriers/shiprocket. As the token, use the same value as Webhook token (x-api-key) in Admin → Settings → Shipping & Delivery.
    Copy each path exactly, and use the same address as in NEXT_PUBLIC_APP_URL. On Vercel, never use a preview or per-deployment *.vercel.app link: those are behind Vercel's login, so every webhook gets 401. On Dokploy or a VPS, an admin screen may show a webhook address that starts with https://localhost:3000. Ignore it and type your real domain.

    Check: Each provider's webhook page shows your https://your-domain/… address. After your test order (below), the provider's delivery log shows a success (status 200).

  7. 7

    Register the social sign-in addresses (only if you use them)

    Skip this step if customers sign in with email and password only. Google and Facebook send people back to your store after sign-in, but only to an address you registered with them.

    1. 1Open Admin → Settings → OAuth / Social Login and turn on Google Sign-In or Facebook Sign-In.
    2. 2Open "How to set this up (step by step)" under the provider. It lists the steps in the provider's console, and shows the address to register with a Copy button.
    3. 3Google: add https://your-domain/api/auth/callback/google under Authorized redirect URIs in your Google Cloud OAuth client.
    4. 4Facebook: add https://your-domain/api/auth/callback/facebook under Valid OAuth Redirect URIs (Facebook Login → Settings in your Meta app).
    5. 5Back in Storify, paste the Google Client ID and Client Secret (Facebook: App ID and App Secret), save, then click Verify credentials.
    6. 6Sign out, open https://your-domain/login and click Continue with Google (or Continue with Facebook).
    Storify builds these addresses from BETTER_AUTH_URL. If you change the domain later, register the new addresses at Google and Meta too.

    Check: You come back to the store, signed in.

  8. 8

    Place one real test order

    A real order proves that payment, webhooks, email and stock work together. Use a cheap product and a real card, then refund the order.

    1. 1Open the storefront in a private browser window, add a product to the cart and check out as a customer, with an email address you can read.
    2. 2Pay with a real card or wallet. If you like, try the gateway's test mode first, then repeat in live mode.
    3. 3In Admin → Orders, open the new order.
    4. 4If you offer refunds, refund the order from its page.
    If the payment succeeds but the order stays pending, the gateway's webhook is not arriving: check it against "Point webhooks at the live domain" above, and see Troubleshooting.

    Check: The order shows the payment status Paid, the order confirmation email arrives, and after the refund the payment status changes to Refunded.

  9. 9

    Turn off demo mode and remove demo logins

    Demo mode blocks deletes and settings changes. The demo accounts that pnpm db:seed and pnpm db:seed:users create have passwords anyone can look up. Neither belongs on a live store.

    1. 1In your environment variables, delete DEMO_MODE and NEXT_PUBLIC_DEMO_MODE, or set them to false. Also delete ADMIN_PASSWORD if it is there.
    2. 2Rebuild as in "Confirm the final address and HTTPS" above, because NEXT_PUBLIC_DEMO_MODE is fixed into the app when it is built.
    3. 3Only if you ran pnpm db:seed or pnpm db:seed:users: sign in as admin@storify.com and change its password in your admin profile. If Admin → Team marks this account as Owner, it cannot be removed, so give it a strong password.
    4. 4Change the passwords of the other demo accounts, or suspend or remove them: vendor@storify.com, staff@storify.com, customer@storify.com and the …@example.com accounts.

    Check: /login shows no "Demo account login credentials" card, and signing in with admin@storify.com and Admin@123 fails.

  10. 10

    Set up backups

    A backup is a copy of your data that you can restore after a mistake or a failure. Orders, customers and settings live in MongoDB. Images live in your storage bucket, not in the database backup.

    1. 1MongoDB Atlas: the Free tier (M0) has no backups. For a live store, move to a paid tier and turn on its backups (the exact labels may differ slightly).
    2. 2Dokploy MongoDB service: set up the Backups tab as shown in Deploy with Dokploy, step "Back up the database". Then click Run Manual Backup once.
    3. 3Atlas or your own server: you can also run mongodump on a schedule. It is part of the free MongoDB Database Tools. Use the command below with your own connection string.
    4. 4Save every environment value (secrets, keys, URLs) in your password manager. Vercel Secret values cannot be read back later.
    Manual backup with mongodump (put your own connection string in the quotes)
    bash
    mongodump --uri="mongodb+srv://USER:PASSWORD@cluster0.xxxxx.mongodb.net/storify" --archive=storify-backup.gz --gzip
    To restore a mongodump file, run mongorestore with --archive=storify-backup.gz --gzip and the --uri of the database to restore into. Practise once on a test database, so you know it works before you need it.

    Check: Dokploy: after Run Manual Backup, a new file appears in your backup bucket. Atlas: within a day, your cluster's backup page lists a snapshot. mongodump: the file storify-backup.gz exists and is not empty.

  11. 11

    Do a security pass

    Go through Security & Hardening once before launch. The most important items are below.

    1. 1Admin → Settings → Security & Access Control: set the password policy.
    2. 2Admin → Settings → Two-Factor Authentication: turn on Enable Two-Factor Authentication and Enable for Administrators, save, then set up two-factor sign-in on your own account.
    3. 3Keep your GitHub repository private, and never commit .env.
    4. 4Make sure payment keys and webhooks are in live mode, not test mode.
    5. 5Give every person their own account (Admin & Roles). Never share the Owner login.

    Check: You can tick off every item on the Security & Hardening page.

  12. 12

    Configure the rest of your store

    Your store is ready for customers. Settings Reference lists every setting and where to find it. Most stores set these up next:

    1. 1Payments: Payment Gateways.
    2. 2Shipping: Shipping Carriers.
    3. 3Look and feel: Branding & Theme.
    4. 4Languages: Multi-language.
    5. 5Your team: Admin & Roles.

Chapter 15 · Scheduled Jobs

Keep the 14 background jobs running

Storify has 14 background jobs. A background job (a cron job) is a task that runs on a timer, for example every minute or once a day. The jobs retry emails and messages that failed, hand paid orders to carriers, expire unfinished checkouts, check mobile-money payments and more. If they stop, the store still looks fine, but this work silently stops. Vercel runs them for you from the bundled vercel.json file (Pro plan). On Dokploy, a VPS or any other host, you schedule them yourself, once.

Before you start

The 14 jobs

Route (/api/cron/…)Schedule (UTC)What stops if it never runs
messaging-outbox* * * * *: every minuteChannel replies (WhatsApp, Messenger, Instagram, Telegram) whose first send failed are never retried
carrier-shipments* * * * *: every minutePaid orders are not handed to the carrier automatically, and address holds never send reminders or reach their deadline
email-deliveries*/5 * * * *: every 5 minutesEmails and text messages that failed their first attempt are never retried
messaging-escalations*/5 * * * *: every 5 minutesUnanswered conversations never escalate
boosts*/5 * * * *: every 5 minutesBoost bookings don't change status, lapsed holds aren't released, reminders aren't sent
orange-money-reconcile*/15 * * * *: every 15 minutesOrange Money payments whose callback never arrived stay pending
mtn-momo-reconcile*/15 * * * *: every 15 minutesMTN MoMo payments whose callback never arrived stay pending
abandoned-checkouts*/15 * * * *: every 15 minutesIdle checkouts are not marked abandoned and recovery emails never send
carrier-tracking*/30 * * * *: every 30 minutesParcel tracking goes stale
checkout-expiry*/30 * * * *: every 30 minutesA gateway checkout the shopper never finished stays pending, and mobile-money orders keep holding stock
vendor-subscriptions0 * * * *: every hour, on the hourVendor plan renewals and expiries are not processed
gateway-disputes40 * * * *: every hour, at minute 40A chargeback whose webhook never arrived is not recorded
finance0 3 * * *: every day at 03:00Recurring expenses are not created and missed ledger postings are not repaired
preorders0 4 * * *: every day at 04:00Pre-order balances and expiries are not processed

Each job is a web address: GET https://your-domain.com/api/cron/<route> with the header Authorization: Bearer <CRON_SECRET>. Times are UTC.

The same jobs as 8 schedules

Schedule nameCronRoutes
storify-every-minute* * * * *messaging-outbox carrier-shipments
storify-every-5-min*/5 * * * *email-deliveries messaging-escalations boosts
storify-every-15-min*/15 * * * *orange-money-reconcile mtn-momo-reconcile abandoned-checkouts
storify-every-30-min*/30 * * * *carrier-tracking checkout-expiry
storify-hourly0 * * * *vendor-subscriptions
storify-disputes40 * * * *gateway-disputes
storify-finance0 3 * * *finance
storify-preorders0 4 * * *preorders

Dokploy uses this grouping: one schedule per row, and each schedule calls every route in its row.

  1. 1

    Set CRON_SECRET wherever the store runs

    Every job must send the header Authorization: Bearer <CRON_SECRET>, and the store refuses calls without the right value. Set the variable on your host first, then use the same value in your scheduler.

    1. 1No value yet? Create one with the command below. It prints 64 random characters.
    2. 2Your computer: add CRON_SECRET=… to .env, then stop pnpm dev (Ctrl+C) and start it again.
    3. 3Vercel: Settings → Environment Variables → add CRON_SECRET → Deployments → ⋯ → Redeploy.
    4. 4Dokploy: the application's Environment tab → add CRON_SECRET=… → Save → Reload.
    5. 5VPS: add CRON_SECRET=… to ~/app/.env, then run sudo systemctl restart storify.
    bash
    node -e "console.log(require('crypto').randomBytes(32).toString('hex'))"
    Never keep the .env.example value replace-with-a-long-random-secret: it works as a real secret, and anyone who reads the example file knows it. The value must match exactly on both sides, with no spaces or line breaks.
  2. 2

    On Vercel: check that the jobs are listed

    Nothing to add: the bundled vercel.json holds all 14 jobs, and Vercel sends your CRON_SECRET with every call. A live store needs the Pro plan; the Hobby plan allows only daily jobs. Vercel runs the jobs only on the production deployment, in UTC.

    1. 1After the first production deploy, open the project → Settings → Cron Jobs.
    2. 2Check that 14 jobs are listed.
    3. 3Click View Logs next to /api/cron/messaging-outbox and look at the status of the latest runs.
    On Hobby, the deploy fails with Hobby accounts are limited to daily cron jobs. This cron expression would run more than once per day. For a private test only, delete the whole "crons" block from vercel.json, commit and push, and schedule the jobs with an external service (see below). Details: Deploy to Vercel.

    Check: Settings → Cron Jobs lists 14 jobs, and the logs for messaging-outbox show status 200.

  3. 3

    On Dokploy: add 8 schedules

    Dokploy runs nothing by itself. Add one schedule for each row of the second table. Each schedule runs a small Node.js command inside the app container, where CRON_SECRET is already set. Deploy with Dokploy has the same recipe.

    1. 1Open your Storify application in Dokploy and go to its Schedules tab.
    2. 2Click Add Schedule, fill in the fields below, and click Create Schedule.
    3. 3Repeat for all 8 rows. Select and copy only the one node -e … line for this schedule, without the # line above it: the block's Copy button copies all eight commands at once. Deploy with Dokploy, step "Schedule the 14 background jobs", has one block per schedule if you prefer to use Copy.
    Task Name
    The schedule name from the table, for example storify-every-minute.
    Schedule
    The cron expression from the same row. Type it into the Custom cron expression box, for example */5 * * * *.
    Timezone
    Leave empty (UTC).
    Shell Type
    Bash.
    Command
    The line from the block below for this schedule. Paste it exactly as it is.
    Enabled
    On.
    One command per schedule
    bash
    # storify-every-minute   (* * * * *)
    node -e "Promise.all(process.argv.slice(1).map(p=>fetch('http://localhost:3000/api/cron/'+p,{headers:{authorization:'Bearer '+process.env.CRON_SECRET}}).then(r=>{console.log(p,r.status);if(!r.ok)process.exitCode=1})))" messaging-outbox carrier-shipments
    
    # storify-every-5-min   (*/5 * * * *)
    node -e "Promise.all(process.argv.slice(1).map(p=>fetch('http://localhost:3000/api/cron/'+p,{headers:{authorization:'Bearer '+process.env.CRON_SECRET}}).then(r=>{console.log(p,r.status);if(!r.ok)process.exitCode=1})))" email-deliveries messaging-escalations boosts
    
    # storify-every-15-min   (*/15 * * * *)
    node -e "Promise.all(process.argv.slice(1).map(p=>fetch('http://localhost:3000/api/cron/'+p,{headers:{authorization:'Bearer '+process.env.CRON_SECRET}}).then(r=>{console.log(p,r.status);if(!r.ok)process.exitCode=1})))" orange-money-reconcile mtn-momo-reconcile abandoned-checkouts
    
    # storify-every-30-min   (*/30 * * * *)
    node -e "Promise.all(process.argv.slice(1).map(p=>fetch('http://localhost:3000/api/cron/'+p,{headers:{authorization:'Bearer '+process.env.CRON_SECRET}}).then(r=>{console.log(p,r.status);if(!r.ok)process.exitCode=1})))" carrier-tracking checkout-expiry
    
    # storify-hourly   (0 * * * *)
    node -e "Promise.all(process.argv.slice(1).map(p=>fetch('http://localhost:3000/api/cron/'+p,{headers:{authorization:'Bearer '+process.env.CRON_SECRET}}).then(r=>{console.log(p,r.status);if(!r.ok)process.exitCode=1})))" vendor-subscriptions
    
    # storify-disputes   (40 * * * *)
    node -e "Promise.all(process.argv.slice(1).map(p=>fetch('http://localhost:3000/api/cron/'+p,{headers:{authorization:'Bearer '+process.env.CRON_SECRET}}).then(r=>{console.log(p,r.status);if(!r.ok)process.exitCode=1})))" gateway-disputes
    
    # storify-finance   (0 3 * * *)
    node -e "Promise.all(process.argv.slice(1).map(p=>fetch('http://localhost:3000/api/cron/'+p,{headers:{authorization:'Bearer '+process.env.CRON_SECRET}}).then(r=>{console.log(p,r.status);if(!r.ok)process.exitCode=1})))" finance
    
    # storify-preorders   (0 4 * * *)
    node -e "Promise.all(process.argv.slice(1).map(p=>fetch('http://localhost:3000/api/cron/'+p,{headers:{authorization:'Bearer '+process.env.CRON_SECRET}}).then(r=>{console.log(p,r.status);if(!r.ok)process.exitCode=1})))" preorders
    A 401 in the run log means CRON_SECRET is missing from the Environment tab: add it, click Reload, and run the schedule again. The command uses Node.js instead of curl because some Storify container images have no curl.

    Check: The Schedules tab lists 8 schedules. Click the play button (Run Manual Schedule) on storify-every-minute, then the list icon next to it: the run log shows messaging-outbox 200 and carrier-shipments 200, and ends with Command executed successfully.

  4. 4

    On a VPS: add the crontab

    A crontab is the list of timed commands of one Linux user. Add all 14 jobs to the crontab of the user that runs Storify. This is the same block as in Deploy to a VPS. It also works on any other Linux server that can reach your store.

    1. 1Run crontab -e. If it asks you to choose an editor, type 1 (nano) and press Enter.
    2. 2Paste the block at the end of the file. Replace YOUR-CRON-SECRET with your CRON_SECRET value and https://your-domain.com with your store address. Save with Ctrl+O, then Enter, then Ctrl+X.
    3. 3Run crontab -l to see what was saved.
    crontab -e
    bash
    CRON_SECRET=YOUR-CRON-SECRET
    BASE=https://your-domain.com
    * * * * *    curl -fsS -H "Authorization: Bearer $CRON_SECRET" $BASE/api/cron/messaging-outbox > /dev/null
    * * * * *    curl -fsS -H "Authorization: Bearer $CRON_SECRET" $BASE/api/cron/carrier-shipments > /dev/null
    */5 * * * *  curl -fsS -H "Authorization: Bearer $CRON_SECRET" $BASE/api/cron/email-deliveries > /dev/null
    */5 * * * *  curl -fsS -H "Authorization: Bearer $CRON_SECRET" $BASE/api/cron/messaging-escalations > /dev/null
    */5 * * * *  curl -fsS -H "Authorization: Bearer $CRON_SECRET" $BASE/api/cron/boosts > /dev/null
    */15 * * * * curl -fsS -H "Authorization: Bearer $CRON_SECRET" $BASE/api/cron/orange-money-reconcile > /dev/null
    */15 * * * * curl -fsS -H "Authorization: Bearer $CRON_SECRET" $BASE/api/cron/mtn-momo-reconcile > /dev/null
    */15 * * * * curl -fsS -H "Authorization: Bearer $CRON_SECRET" $BASE/api/cron/abandoned-checkouts > /dev/null
    */30 * * * * curl -fsS -H "Authorization: Bearer $CRON_SECRET" $BASE/api/cron/carrier-tracking > /dev/null
    */30 * * * * curl -fsS -H "Authorization: Bearer $CRON_SECRET" $BASE/api/cron/checkout-expiry > /dev/null
    0 * * * *    curl -fsS -H "Authorization: Bearer $CRON_SECRET" $BASE/api/cron/vendor-subscriptions > /dev/null
    40 * * * *   curl -fsS -H "Authorization: Bearer $CRON_SECRET" $BASE/api/cron/gateway-disputes > /dev/null
    0 3 * * *    curl -fsS -H "Authorization: Bearer $CRON_SECRET" $BASE/api/cron/finance > /dev/null
    0 4 * * *    curl -fsS -H "Authorization: Bearer $CRON_SECRET" $BASE/api/cron/preorders > /dev/null
    The times follow the server's clock, which is UTC on most VPS images. Check it with timedatectl.

    Check: crontab -l lists the two settings lines and all 14 job lines.

  5. 5

    Anywhere else: use an external scheduler

    Any service that can call a web address on a timer works, for example cron-job.org. Create one job for each of the 14 routes in the first table, with the settings below. The labels differ from service to service.

    1. 1Choose a service that can run a job every minute, because two jobs need that.
    2. 2Set the service's time zone to UTC, or convert the daily times (03:00 and 04:00 UTC) to your time zone.
    3. 3Vercel Hobby (private tests only): delete the "crons" block from vercel.json first, then use this step.
    URL
    https://your-domain.com/api/cron/<route>, for example https://your-domain.com/api/cron/email-deliveries.
    Method
    GET.
    Header name
    Authorization.
    Header value
    Bearer YOUR-CRON-SECRET (the word Bearer, one space, then your secret).
    Schedule
    The schedule of that route in the first table.
    Use the store's final address. If the address redirects (for example your-domain.com to www.your-domain.com), a scheduler may not follow the redirect or may drop the header, and every run then fails.

    Check: The service's run history shows status 200 for each job.

  6. 6

    Test one job by hand

    Call one job yourself to prove that the secret is right. email-deliveries is safe to run at any time: it only retries emails and texts that are waiting.

    macOS, Linux, or Windows with WSL
    bash
    curl -i -H "Authorization: Bearer YOUR-CRON-SECRET" https://your-domain.com/api/cron/email-deliveries
    Windows PowerShell
    powershell
    Invoke-WebRequest -UseBasicParsing -Uri "https://your-domain.com/api/cron/email-deliveries" -Headers @{ Authorization = "Bearer YOUR-CRON-SECRET" }
    A 401 with {"success":false,"message":"Unauthorized"} means CRON_SECRET is missing on the host or differs from the value you sent. In PowerShell a 401 appears as a red error. A 401 call is not recorded, so the job keeps looking as if it never ran.

    Check: curl shows HTTP/2 200 (or HTTP/1.1 200 OK) and a reply that starts with {"success":true. PowerShell shows StatusCode : 200.

  7. 7

    Read the dashboard notice

    Storify records every successful run. Each time an admin opens the dashboard, it looks for jobs that have never run, have not run for several times their interval, or failed three times in a row. It then sends every admin a notice titled "Background jobs have stopped": in the admin notifications, as a browser push notification (if allowed) and by email (if email is set up).

    1. 1Your first dashboard visit after the installer shows this notice. That is expected: the jobs have not had their first turn yet.
    2. 2The notice lists jobs under "Not running" or "Running but failing every time". A new notice arrives each time that list changes.
    3. 3Jobs that run every 1 to 30 minutes drop off the list within about an hour. The hourly jobs (vendor-subscriptions, gateway-disputes) need up to two hours. The daily jobs (finance at 03:00 UTC, preorders at 04:00 UTC) need up to a day.
    4. 4If the every-minute jobs (messaging-outbox, carrier-shipments) are still named after an hour, the schedule is not running. Go back to the step for your host, and test one job by hand.

    Check: After an hour, a new "Background jobs have stopped" notice names at most vendor-subscriptions, gateway-disputes, finance and preorders. After a day, no new notice appears.

Keep CRON_SECRET private

Anyone who knows it can start the jobs whenever they like. Keep it out of screenshots, support tickets and public repositories. If it leaks, create a new value and change it on the host and in every scheduler at the same time.

Maintenance mode does not stop the jobs

The jobs keep running while the store is in maintenance mode, so queued emails and payment checks carry on during an update.

Chapter 16 · Custom Domain & SSL

Put your store on your own domain with HTTPS

Give your store its final address, such as https://www.your-domain.com, with a free HTTPS certificate (the padlock in the browser). Vercel, Dokploy and Caddy all get and renew the certificate for you. You add a DNS record, tell your platform about the domain, then switch the store and the services that call it to the new address. Do this before you share the store: sign-in, emails and payment webhooks all use this address.

Before you start

  • Your store is deployed and works on its first address.
  • Access to your domain's DNS settings, at your domain registrar or at Cloudflare.
  1. 1

    Choose the final address

    Pick one address and use exactly that one everywhere: www.your-domain.com, the bare domain your-domain.com, or a subdomain such as shop.your-domain.com. Send the other variant to it with a redirect.

    1. 1Vercel recommends www as the main address, with the bare domain redirecting to it.
    2. 2The bare domain needs an A record. A subdomain (www, shop) uses a CNAME record on Vercel, or an A record on your own server.
    3. 3Payment webhooks and job schedulers often do not follow redirects. Always give them the final address, not the one that redirects.
    4. 4DNS at Cloudflare? Create the records as DNS only (grey cloud) for now. The Cloudflare step below explains when to change that.
    Change the domain before real orders arrive. Then there are no live webhooks or customer links to move later.
  2. 2

    On Vercel: add the domain

    Vercel shows you exactly which DNS records to create and issues the certificate automatically once they are in place. Deploy to Vercel walks through the same screens.

    1. 1Open your project → Settings → Domains → Add Domain.
    2. 2Type your domain. For a bare domain, Vercel offers to add the www version too. Accept it.
    3. 3Vercel shows the records to create: an A record for a bare domain, a CNAME record for a subdomain. At your DNS provider, create exactly those records, and delete any other A, AAAA or CNAME record with the same name.
    4. 4To choose which address redirects: click Edit on that domain and pick your main domain under Redirect to.
    5. 5Wait until the domain shows as valid (Valid Configuration; the label may differ slightly). The certificate follows automatically.
    Copy the values from your own project's screen. Vercel may give each project its own DNS values, so values from other guides (for example cname.vercel-dns.com) may not work for yours.

    Check: Settings → Domains shows no error next to the domain, and https://your-domain opens your store with a padlock.

  3. 3

    On Dokploy: add the domain with Let's Encrypt

    Create the DNS record first, then add the domain in Dokploy. If you add the domain before the DNS record works, the certificate is not created. Full Dokploy guide: Deploy with Dokploy.

    1. 1At your DNS provider, add an A record. Name: shop (or @ for the bare domain). Value: your server's IP address.
    2. 2Wait until nslookup shop.your-domain.com shows your server IP in its last Address line (usually 15 to 30 minutes).
    3. 3Check that Settings → Web Server has a real Let's Encrypt Email.
    4. 4Open your application → Domains → Add Domain, fill in the form below, and click Create.
    5. 5Click Validate DNS next to the domain.
    Host
    shop.your-domain.com
    Path
    /
    Container Port
    3000
    HTTPS
    On.
    Certificate Provider
    Let's Encrypt.
    Let's Encrypt checks your domain over port 80, so port 80 must be open to the internet. If you added the domain before the DNS record worked and there is still no certificate, delete the domain in Dokploy and add it again.

    Check: Validate DNS shows "DNS Valid", and https://shop.your-domain.com opens with a padlock. No redeploy is needed.

  4. 4

    On a VPS with Caddy: add a site block

    Caddy gets and renews the certificate by itself as soon as your domain appears in its configuration and the DNS record points at the server. Full server guide: Deploy to a VPS.

    1. 1At your DNS provider, add an A record with your server's IP address for each name in the block below (here @ and www).
    2. 2Open the file with sudo nano /etc/caddy/Caddyfile. Replace the old address with the new one, as in the block below. The second part is only needed if the www address should redirect to the main one. Save.
    3. 3Load the new configuration: sudo systemctl reload caddy.
    /etc/caddy/Caddyfile
    bash
    your-domain.com {
        reverse_proxy 127.0.0.1:3000
    }
    
    www.your-domain.com {
        redir https://your-domain.com{uri} permanent
    }
    Caddy needs ports 80 and 443 open, in ufw and in your provider's firewall.

    Check: https://your-domain.com opens with a padlock. If not, sudo journalctl -u caddy --no-pager | tail -n 30 shows why.

  5. 5

    Using Cloudflare DNS? Set the proxy correctly

    Cloudflare can pass your visitors through its own servers (proxy status Proxied, an orange cloud) or only answer DNS questions (DNS only, a grey cloud). The wrong setting stops the certificate or causes endless redirects.

    1. 1In Cloudflare, open DNS → Records and edit your store's record. Keep Proxy status on DNS only until the padlock appears on your store.
    2. 2Vercel: keep it on DNS only. Vercel does not recommend a proxy in front of it.
    3. 3Dokploy or VPS: after the padlock appears, you may use the proxy. First set SSL/TLS → Overview → encryption mode to Full (strict). Then switch the record to Proxied.
    4. 4Never use Flexible mode. Your server redirects every visitor to HTTPS, and with Flexible that redirect repeats forever: the browser says the page redirected too many times.

    Check: The store opens with a padlock and no redirect error.

  6. 6

    Switch the store to the new address

    BETTER_AUTH_URL and NEXT_PUBLIC_APP_URL must both be exactly the new address, with https:// and no / at the end. NEXT_PUBLIC_APP_URL is built into the app, so the store must be rebuilt, not only restarted.

    1. 1Vercel: Settings → Environment Variables → edit both values → Deployments → ⋯ → Redeploy, and untick "Use existing Build Cache".
    2. 2Dokploy: the Environment tab → edit both values → Save → Deploy.
    3. 3VPS: nano ~/app/.env → edit both values → save → run the block below. Then run crontab -e and change the BASE= line.
    4. 4External scheduler (cron-job.org and others): change the address in every job.
    VPS: rebuild and restart
    bash
    cd ~/app
    pnpm build
    sudo systemctl restart storify

    Check: Signing in at https://your-domain/login works. A 403 "Invalid origin" means one of the two values still holds the old address, or the rebuild has not run yet.

  7. 7

    Update payment webhooks

    Payment providers call your store at a fixed address. Change it for every provider you use, with your final address in front of each path. The Launch Checklist lists what each provider needs.

    1. 1Stripe: https://your-domain/api/payments/webhook. Edit the existing endpoint's URL. If you create a new endpoint instead, copy its new signing secret into Admin → Settings → Payment Settings.
    2. 2PayPal: https://your-domain/api/payments/paypal/webhook. If you create a new webhook instead of editing the old one, paste its new webhook ID into Payment Settings.
    3. 3Paystack: https://your-domain/api/payments/paystack/webhook.
    4. 4Razorpay: https://your-domain/api/payments/razorpay/webhook.
    5. 5Pesapal: open Admin → Settings → Payment Settings and click Register IPN again. Until you do, Pesapal keeps sending notifications to the old domain. It needs NEXT_PUBLIC_APP_URL to be a public https:// address.

    Check: One test order on the new address is marked as paid in the admin.

  8. 8

    Update sign-in, messaging, shipping and storage

    Other services also know your old address. Update the ones you use.

    1. 1Google and Facebook sign-in: add https://your-domain/api/auth/callback/google and https://your-domain/api/auth/callback/facebook as redirect URIs in each provider's app settings.
    2. 2Meta (WhatsApp, Messenger, Instagram): set the webhook URL to https://your-domain/api/webhooks/meta. Type it yourself: on Dokploy and a VPS, the admin screen may show an address with localhost or 127.0.0.1 in it.
    3. 3Telegram: connect the bot again in Admin → Settings → Omnichannel Messaging, so Storify registers the new address with Telegram. On Dokploy or a VPS, see the Telegram note in the Launch Checklist first.
    4. 4Shippo: in Admin → Settings → Shipping & Delivery, click Generate webhook URL again and paste the new URL into Shippo. Shiprocket: https://your-domain/api/webhooks/carriers/shiprocket.
    5. 5Storage bucket: add the new address to the bucket's CORS rule (see "Create a storage bucket" in Before You Install). Without it, uploads cannot go straight to the bucket and large files fail.

    Check: Signing in with Google or Facebook works, and uploading a product image in the admin works.

Still no certificate after an hour?

Check for an old AAAA record (IPv6) that points somewhere else, a CAA record that does not allow letsencrypt.org, the Cloudflare proxy switched on too early, or port 80 closed (Dokploy and Caddy need it).

Chapter 17 · Settings Reference

Complete admin settings reference

Use this as the checklist for Admin -> Settings. It maps all twenty-one 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, Orange Money, MTN MoMo, 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. Also holds Shipping carriers, Saved packages, and Automatic shipping — see Shipping Carriers.

Product Boosting

The opt-in switch for sponsored products, ladder depth on listing and product pages, hold minutes, booking horizon, and maximum booking length. Marketplace-only, and off by default.

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 Cloudflare R2, DigitalOcean Spaces, AWS S3, or MinIO; stores account, endpoint, region, bucket, and access credentials, public CDN URL, upload path prefix, allowed MIME types, size limits, and opens the Media Library. Each provider keeps its own credentials, and per-provider setup steps sit beside the fields they fill in.

Settings route reference (replace en with your locale)
txt
/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
/en/admin/settings/boosting

# 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 catalog

# Added in 1.5
/en/admin/finance                   # profit & loss, cash position, shipping margin
/en/admin/finance/expenses          # expenses and recurring templates
/en/admin/finance/receivables       # held for each vendor vs owed by them
/en/admin/finance/reports           # tax summary and month-end closing
/en/admin/locations                 # inventory locations, outside POS
/en/admin/boosts                    # bookings
/en/admin/boosts/positions          # the position ladder

Settings 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.

Chapter 18 · Storage & Media

Configure media storage

Storify stores product images, 3D models, category thumbnails, blog images, chat attachments, and digital deliverables through one storage service. Four providers are supported from Admin -> Settings -> Storage — Cloudflare R2, DigitalOcean Spaces, AWS S3, and self-hosted MinIO. All four are S3-compatible and served by the same code; they differ only in what you paste into the form. Pick one before you add your first product.

  1. 1

    Choose a provider

    R2 is the default because egress — the bandwidth for serving images to shoppers — is free, and for a store that is the number that matters, not the storage price. Spaces suits a store already on DigitalOcean and includes a CDN. S3 suits an existing AWS footprint. MinIO is the answer if you cannot get an international credit card: it is S3 software you run yourself, on the same VPS as the app.

  2. 2

    Create the bucket

    R2: dashboard -> R2 -> Create bucket. Spaces: create a Space and note its datacenter, which is the region. S3: create a bucket and note its region. MinIO: open the console on port 9001 and create a bucket. Use a lowercase name with numbers and hyphens only.

    txt
    storify-media
  3. 3

    Create scoped access credentials

    R2: an API token with Object Read & Write scoped to that bucket, plus your 32-character Account ID. Spaces: API -> Spaces Keys -> Generate New Key. S3: an IAM user with s3:PutObject, s3:GetObject, s3:DeleteObject, and s3:ListBucket on that bucket only. MinIO: Access Keys -> Create access key in the console.

  4. 4

    Make the bucket publicly readable

    Browsers have to be able to load product images. R2: enable the r2.dev subdomain or, better, connect a custom domain. Spaces: set File Listing to public, or enable the CDN. S3: a public-read bucket policy, or CloudFront in front of it. MinIO: mc anonymous set download myminio/<bucket>.

    MinIO — make the bucket readable
    bash
    mc alias set myminio https://minio.example.com <user> <password>
    mc anonymous set download myminio/<bucket>
  5. 5

    Fill in the form and test the connection

    Every provider's own steps are repeated in the admin, right beside the fields they fill in. Enter the credentials, set the Public URL, then press Test connection — it uploads a probe object and fetches it back over the public URL, which is what catches a bucket that authenticates fine but serves 403 to browsers.

    txt
    https://assets.yourdomain.com
  6. 6

    Set limits and verify with a real upload

    Set the upload path prefix, allowed MIME types, and max image, video, and 3D model sizes. Then upload a product image and a 3D model from the product editor. Admin -> Settings -> Storage also opens a provider-aware Media Library with search, filtering, previews, URL copying, and bulk deletion.

Endpoints and regions per provider
txt
Cloudflare R2      https://<ACCOUNT_ID>.r2.cloudflarestorage.com   region: auto
DigitalOcean       (leave endpoint blank — the datacenter is the region: nyc3, sgp1, fra1 ...)
AWS S3             (leave endpoint blank — set the bucket's region)
MinIO              https://minio.example.com:9000                  region: us-east-1
MinIO alongside the app (docker compose)
yaml
services:
  minio:
    image: minio/minio
    command: server /data --console-address ":9001"
    environment:
      MINIO_ROOT_USER: <pick-a-user>
      MINIO_ROOT_PASSWORD: <pick-a-long-password>
    volumes:
      - minio-data:/data      # the whole point: data outside the container
    ports:
      - "9000:9000"           # API — this is the Endpoint URL
      - "9001:9001"           # web console

volumes:
  minio-data:
Moving off local disk (upgrading from 1.4 or earlier)
bash
# 1. BEFORE deploying, if your update replaces the app folder, copy the media out
cp -r public/uploads private-uploads ~/storify-media-backup/

# 2. Deploy, configure a provider in Settings -> Storage, press Test connection

# 3. Move credentials to the per-provider layout (no-op if already done)
pnpm db:migrate storage-credentials --dry-run
pnpm db:migrate storage-credentials

# 4. Upload every local file to the provider and rewrite the stored URLs
pnpm db:migrate media-to-cloud --dry-run     # lists every file, writes nothing
pnpm db:migrate media-to-cloud

Local disk was retired in 1.5

Files written inside the app folder disappear on any deploy that replaces it, and local disk can never support presigned direct uploads, multiple servers, or a serverless host. An existing store keeps serving what it already has — including digital deliverables customers have paid for — but nothing new uploads until a provider is configured. Run the migration above; a file that fails to upload keeps its local URL, so a partial run leaves a working store.

MinIO needs a mounted volume and HTTPS

Without minio-data:/data a redeploy recreates the container and every file in it is gone — the same failure that retired local storage. Put MinIO behind your reverse proxy on its own subdomain too: browsers refuse mixed content, so an http:// endpoint breaks images on an https:// storefront.

Keep 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. Keep the bucket private if you sell digital products — a public bucket, or an R2 custom domain, exposes every key it holds. See Products & Downloads.

Persist the Next.js image cache

Next optimizes each image once and caches it in .next/cache/images. Most deploys throw that away, so the first requests after every release re-encode the whole catalog through sharp — CPU at 100% exactly when traffic returns. Mount .next/cache on a volume that survives deploys. Changing storage provider does not help; R2 and disk both go through the same optimizer.

Chapter 19 · Payment Gateways

Configure checkout payments

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. 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 fields
    txt
    Publishable Key
    Secret Key
    Webhook Secret
    Test connection
  2. 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.

    bash
    https://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. 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 events
    txt
    checkout.session.completed
    checkout.session.expired
    customer.subscription.created
    customer.subscription.updated
    customer.subscription.deleted
    invoice.paid
    invoice.payment_failed
    invoice.payment_action_required
  4. 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 fields
    txt
    PayPal:   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. 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. 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.

Chapter 20 · Shipping Carriers

Buy real labels with Shippo & Shiprocket

Turn a paid order into a real parcel: rate-shop a consignment, buy a carrier label, print the AWB, and keep tracking in sync. Shippo covers the world (USPS, UPS, FedEx, DHL and 40+ more); Shiprocket covers India. Configure both from Admin -> Settings -> Shipping -> Shipping carriers.

  1. 1

    Complete the ship-from address first

    Settings -> Shipping -> Origin needs street, city, postcode, country, and a phone. Carriers reject anything less, and a free-text store address is not a substitute. A state or province is required only for US, CA, AU, and IN — much of the world has no subdivision. A vendor may override the origin with its own.

  2. 2

    Connect Shippo

    Create an account at goshippo.com and copy both API tokens. There is no sandbox host — the token itself decides whether a label is real, which is why both are stored and Mode selects between them. Press Test connection to confirm the account authenticates before enabling it.

    bash
    SHIPPO_MODE=test
    SHIPPO_TEST_TOKEN=shippo_test_xxx
    SHIPPO_LIVE_TOKEN=shippo_live_xxx
  3. 3

    Connect Shiprocket

    Shiprocket needs an API-user login (created under Settings -> API in their panel, not your dashboard login) and a pickup-location nickname registered in their dashboard. It only dispatches from India. There is no sandbox at all — a working login is a live account, and every label it issues is real.

    bash
    SHIPROCKET_EMAIL=api-user@example.com
    SHIPROCKET_PASSWORD=your-shiprocket-api-password
    SHIPROCKET_PICKUP_LOCATION=Primary
  4. 4

    Set up saved packages

    Carriers price by volume as well as weight. Settings -> Shipping -> Saved packages holds the box catalog; one default box ships with a fresh install. The packer picks the smallest saved box the contents fit in, orientation-free, then the default, then the largest. A consignment heavier than every box ships in the largest with an OVERWEIGHT warning rather than a guessed split.

  5. 5

    Register the webhooks

    Press Generate webhook URL in the settings card and paste it into the Shippo dashboard; for Shiprocket set the same webhook token in both systems. The body is never trusted — only the object id is read, and the worker re-fetches authoritative state with your own token, so a leaked URL achieves nothing beyond making Storify poll its own account.

    txt
    Shippo      /api/webhooks/carriers/shippo/<secret>   secret in the path
    Shiprocket  /api/webhooks/carriers/shiprocket        x-api-key header
  6. 6

    Ship a parcel manually

    The Shipments card on the admin and vendor order pages has Send to courier. Choose a box (or let the packer decide), rate-shop, then buy. The label, AWB, and tracking URL are written to the shipment, the tracking number and courier are recorded on the sub-order and the order, and unless Mark the order shipped is off the order moves to shipped — which is what sends the customer their tracking email.

  7. 7

    Turn on automation

    Settings -> Shipping -> Automatic shipping ships a parcel once the sub-order is processing and the order is paid, subject to an order-total window and a destination allow-list. Rate choice is cheapest, fastest, or a fixed service. Buy the label automatically can be switched off to produce a rate-shopped draft and stop. COD orders are excluded unless Include cash-on-delivery orders is on, since a COD order is unpaid by definition.

  8. 8

    Run the migrations and the crons

    Automation and tracking sync both need CRON_SECRET and the two cron entries already present in vercel.json. Run the three migrations after deploying the code that changed the schemas.

    bash
    pnpm db:migrate carrier-shipments    # Shipment + ShipmentJob indexes
    pnpm db:migrate suborder-carrier     # backfill SubOrder.carrier
    pnpm db:migrate webhook-provider     # WebhookEvent TTL + retention
    
    # Every migration takes --dry-run. Run that pass first.
vercel.json cron entries
json
{ "path": "/api/cron/carrier-shipments", "schedule": "* * * * *" },
{ "path": "/api/cron/carrier-tracking",  "schedule": "*/30 * * * *" }

Live tokens buy real labels

A live Shippo token and any Shiprocket login are billed to your carrier account the moment a label is purchased. Test everything on Shippo's test token first. Shipment.providerMode records which environment bought each label, so a test label can never be mistaken for a real one and the order screen badges it.

Checkout is deliberately untouched

Shoppers are still priced by the store's own zones and rates. Carriers are a fulfillment feature — they turn a paid order into a parcel with a real AWB. Carrier rates are stored verbatim in the carrier account's currency, never converted, and never touch order totals or vendor payouts. Live carrier rates at checkout are not part of this release.

Vendors can bring their own carrier account

Vendor -> Settings -> Shipping -> Carrier account. Those tokens are encrypted at rest with CARRIER_ENCRYPTION_KEY, falling back to MESSAGING_ENCRYPTION_KEY. The store's per-carrier enabled switch stays policy — a carrier the store switched off stays off for everyone — while a vendor account overrides only the credentials. A half-configured override is ignored rather than applied.

Shiprocket quotes from its own pickup pincode

Shiprocket collects from a registered location whose address lives in their dashboard, not from Settings -> Shipping -> Origin. If the nickname is not on the account, the rates step says so. Rate shopping calls only courier/serviceability, so an abandoned lookup never leaves a phantom consignment in your panel.

Not supported in this release

Return/RMA labels, multi-parcel splitting, pickup scheduling, batch labels and manifests, blocking address validation at checkout, insurance, customs paperwork beyond the auto-derived declaration, carrier-account OAuth onboarding, rate markup and handling fees, and live carrier rates at checkout.

Chapter 21 · Email / SMTP

Wire up transactional email

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. 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. 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.

    bash
    curl -H "Authorization: Bearer $CRON_SECRET" \
      https://yourdomain.com/api/cron/email-deliveries
  3. 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.

Chapter 22 · AI Studio & Sales Agent

Set up AI authoring and the shopper assistant

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. 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.

    bash
    OPENAI_API_KEY=your-openai-api-key-here
  2. 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. 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. 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. 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.

    bash
    AI_HERO_BANNER_ENABLED=true
  6. 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.

Chapter 23 · Omnichannel Messaging

Connect chat, WhatsApp, Messenger, Instagram & Telegram

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. 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.

    bash
    MESSAGING_ENCRYPTION_KEY=at-least-32-random-characters
    CRON_SECRET=a-long-random-value
  2. 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. 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.

    bash
    https://yourdomain.com/api/webhooks/meta
  4. 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. 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. 6

    Connect Telegram

    Create a bot with BotFather and paste its token into the Telegram channel panel. Storify registers the webhook for you.

    bash
    https://yourdomain.com/api/webhooks/telegram
  7. 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.

    bash
    GET /api/cron/messaging-outbox        # every minute
    GET /api/cron/messaging-escalations   # every 5 minutes
  8. 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.

    bash
    pnpm db:migrate omnichannel --dry-run && 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.

Chapter 24 · Vendors & Subscriptions

Run a marketplace with optional paid plans

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 any enabled gateway, with per-plan commission rates and limits.

  1. 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. 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. 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. 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. 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. 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.

    bash
    curl -H "Authorization: Bearer $CRON_SECRET" \
      https://yourdomain.com/api/cron/vendor-subscriptions
  7. 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.

    bash
    pnpm db:migrate vendor-billing --dry-run && 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.

Plans are no longer Stripe-only

Vendors pay through whatever the allow-list, the enabled flags, the credentials, and the store currency permit — so a store whose vendors pay by Pesapal or ioTec can charge them too. A vendor already on Stripe can move off it: the next period is bought through the new gateway first and only then is Stripe cancelled, so an abandoned checkout changes nothing and no paid day is lost. Vendor -> Billing shows their payment history.

Changing the commission rate reaches existing vendors

Vendor.commission is an enforcement cache the money path reads directly, so editing the rate in Settings used to change nothing for vendors who already existed. It now sweeps the ones still on the store default and leaves individually negotiated rates alone. Run pnpm db:migrate commission-source once to classify existing rows; plans are deliberately not swept, since a subscriber's rate is frozen as the terms they signed up under.

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.

Chapter 25 · Finance & Ledger

Report money from a double-entry ledger

Admin -> Finance reports from a ledger rather than by re-counting orders. Every paid order, refund, payout, expense, boost payment, and carrier label posts a pair of entries, and profit and loss, cash position, and vendor balances are all groupings of those entries — so two screens cannot disagree about the same sale.

  1. 1

    Backfill the ledger before you trust a figure

    On an existing store the ledger starts empty, so without this every report begins on the day you upgraded and the whole section looks broken rather than new. The backfill replays paid orders, refunds, payouts, and labels. It is safe to run repeatedly — entry keys come from the source documents, so a second run collides with itself and writes nothing.

    bash
    pnpm db:migrate ledger --dry-run   # counts what it would post, writes nothing
    pnpm db:migrate ledger
  2. 2

    Read the Overview

    Income by account, costs by account, net, cash held at the gateway, the bank and the register, and the shipping margin. The period and book live in the URL, so a finance figure can be linked to and comes back the same. Order volume sits apart from revenue and is labelled as volume — on a marketplace most of what flows through the store belongs to vendors.

    txt
    /en/admin/finance
  3. 3

    Record expenses

    Rent, salaries, and advertising are the money events nothing in the app produces on its own; without them, profit is revenue minus whatever costs happen to pass through a gateway. Categories are fixed because each maps to an account. Switch Repeats on for a recurring template, and the daily cron creates each copy when it falls due — dated when it was due, not when the job ran.

    txt
    /en/admin/finance/expenses
  4. 4

    Settle with vendors

    Receivables shows what the platform holds for each vendor beside what that vendor owes on cash they collected themselves. The two offset, so the settlement figure is the net — pay the gross and chase the commission separately and the marketplace has quietly lent to its own seller. Both directions are read from the ledger, not from Payout rows, so the vendor's own screens cannot disagree.

    txt
    /en/admin/finance/receivables
  5. 5

    Close a month

    Finance -> Reports carries the tax summary — collected, refunded, and the difference, per currency — and month-end closing. Closing locks nothing: entries stay append-only, and anything arriving for a closed month lands on the first instant after the close carrying a note saying where it belonged. The totals at the moment of closing are stored, so 'why is March different now?' has an answer.

    txt
    /en/admin/finance/reports
  6. 6

    Give vendors their own view

    Vendor -> Finance has an overview, a bank-statement-style ledger with opening and closing balances, expenses, payouts, and what they owe the platform, listed order by order. Both the statement and their costs export to CSV with a real UTF-8 BOM, so the figure an accountant sees is not re-typed from a screen.

  7. 7

    Wire the cron

    Recurring expenses need CRON_SECRET and the finance cron entry already present in vercel.json. Without the secret the endpoint answers 401 and no copy is ever created.

    json
    { "path": "/api/cron/finance", "schedule": "0 3 * * *" }

Two books, never added together

own is the store's own stock, where the shop is the seller and books full revenue and cost of goods. marketplace is what is earned as an agent — commission, subscriptions, boosts — where a vendor's share is money held on their behalf and never revenue. With multi-vendor mode off, the marketplace book is simply never written to.

Currencies are never summed

Every total is grouped by the currency it was recorded in. A missing figure is under its own currency, not converted into a single total. A payout moves money in the currency the sales were made in, and a period spanning two currencies is refused rather than added together.

Nothing is edited, only reversed

Correcting or deleting an expense posts a mirror entry rather than changing the original, so a report you ran last month still produces last month's answer. An expense is the only editable financial record, because it is the only one a human types.

Not everything in a total is revenue

An order's total splits into goods, delivery, tax, and import duty before anything posts, and each lands in its own account. Tax and duty are liabilities owed onward; only the goods are shared with a vendor. A part-paid pre-order posts the sale in full and the uncollected balance as Owed by customers.

After a posting rule changes, rebuild — do not re-run

Ledger keys come from the source document, which makes replays safe and has one consequence: an entry written under an older rule keeps its key, so a plain re-run collides with it and the old figure survives forever. Use pnpm db:migrate ledger -- --rebuild (dry run first, always). On any other day it deletes good data to write the same thing back, which is why it is a separate flag.

If you backfilled on an earlier version, rebuild once

The order and refund rules changed after the first ledger release — duty and a free-shipping coupon come out of the merchandise a vendor is paid for, a refund reverses each part where it came from, and a part-paid pre-order no longer books cash that has not arrived. Run pnpm db:migrate ledger -- --rebuild once to replace entries written under the old rules.

A refund landing after a payout is recovered from the next one

A payout is final and a refund is not, so a shopper returning goods a month after the vendor was paid leaves the platform out of pocket. The difference is carried forward as an Adjustment on the next payout — never more than that payout is worth, with the remainder coming off the one after — and shown on the vendor's payout detail, so the deduction is never an unexplained gap.

The run tells you if it went wrong

Every backfill ends by printing the trial balance and exits non-zero if it is not zero, so a rebuild that went wrong says so rather than leaving you to find it in a report weeks later.

Chapter 26 · Product Boosting

Sell sponsored positions to vendors

Vendors book a numbered position on a marketplace-wide ladder for a range of days and pay per day. This is a revenue stream for the marketplace owner and a visibility lever for vendors. It is opt-in, off by default, and needs multi-vendor mode.

  1. 1

    Run the migrations first

    The feature is inert without the first, and non-Stripe vendor renewals are billed in the wrong currency without the second. Vendor permissions are materialized per row at creation, so existing vendors do not pick up new permission keys from a code upgrade — without the third they get 'forbidden' on Vendor -> Boosts.

    bash
    pnpm db:migrate boosts                 # indexes for sponsored placements
    pnpm db:migrate boost-permissions      # grant view/manage_boosts to existing vendors
    pnpm db:migrate subscription-currency  # repair plan snapshots stamped "USD"
    
    # Add --dry-run to any of them to report without writing.
  2. 2

    Turn it on

    Admin -> Settings -> Product Boosting holds the opt-in switch, ladder depth on listing and product pages, the hold window, the booking horizon, and the maximum booking length. Multi-vendor mode must be on — boosting is a marketplace feature.

    txt
    /en/admin/settings/boosting
  3. 3

    Define the ladder

    Admin -> Boosting -> Positions is a vertical list of rungs, each with its own price per day. Each card shows the 7- and 30-day totals, an occupancy strip over the next 30/60/90 days, and its average impressions per day — occupancy is the pricing instrument, since a rung booked solid 60 days out is underpriced and one at zero is overpriced.

    txt
    /en/admin/boosts/positions
  4. 4

    Let vendors book

    The vendor purchase dialog picks a rung and a date range on one screen, disables days already sold, and prints the disclosures — including that the slot does not move up and that filtered or search results never show sponsored products. The total is computed server-side as pricePerDay x days; the figure the dialog shows is display-only.

  5. 5

    Schedule the cron

    The cron advances status, sends notifications, releases lapsed holds, and issues delivery credits. Rendering does not depend on it — a scheduled booking starts at exactly 00:00 UTC with no cron tick and no cache bust.

    json
    { "path": "/api/cron/boosts", "schedule": "*/5 * * * *" }

Strict-index rendering

Position N renders at visual slot N. If Position 2 is unsold, slot 2 shows an ordinary product and Position 3 does not move up into it. This is the commercial foundation: if unsold positions compacted, every vendor would buy the cheapest rung and still land on top, and the price ladder would collapse. A gap is what the vendor above paid for.

Depth per surface

The ladder is one global ordering, but the surfaces are not the same size — home rail 8 by default, shop and category listings 2, product-page rail 8. A position deeper than every depth renders nowhere at all and is unsellable; the ladder screen marks it and the purchase dialog withholds it.

What global means, precisely

A booked product appears on every home page rendering the sponsored rail, every product detail page except its own, the main shop listing, and its own category listing. It does not appear in results narrowed by a filter, search term, brand, collection, price range, or location — those are answering a shopper's question. The scope is disclosed verbatim in the purchase dialog.

Inventory is a row, never a flag

A day is sold if and only if a slot-day row exists for that position and day, so a duplicate-key error is the 'already taken' answer and two vendors cannot buy the same day. Days are held before the gateway is contacted, so a conflict names the taken days and creates no payment attempt.

Nothing bills on impressions

What a vendor buys is a visual slot for a range of days — not an impression count, not a reach guarantee, not a budget. Impression and click figures are reporting-grade only, which is why light bot filtering is enough.

A product going dark hands its days back

Unpublishing or archiving a product, deleting it, or suspending a vendor releases the future days and credits the vendor, rather than leaving a dead booking to expire weeks later and burning a rung nobody else can buy. Today is never released — its row is the delivery record. The product form warns with the day count and the credit before the save, because releasing is immediate and a competitor can take the days the same second.

Clear a pre-release flat-fee catalog once

If you ran an early build that sold flat-fee boost packages, those rows carry no position and no booked days, so every lifecycle path reads as broken while they sit there. Run pnpm db:migrate drop-boost-packages --dry-run, then apply.

Deeper reference

The bundled /docs/PRODUCT_BOOSTING.md covers the ladder model, depth arithmetic with worked examples, the booking and refund rules, and the delivery-credit sweep.

Chapter 27 · POS, Inventory & Labels

Set up the register, barcodes and printing

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. 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. 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. 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. 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.

    bash
    pnpm db:migrate barcodes --dry-run
    # resolve duplicates, then
    pnpm db:migrate barcodes
  5. 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. 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. 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.

    bash
    QZ_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.

Chapter 28 · Products & Downloads

Physical products, variants and digital files

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.

Keep the bucket private if you sell digital products

Deliverables go to a separate private key prefix and are only ever handed out as short-lived signed URLs — but a bucket with blanket public access, or an R2 custom domain in front of it, exposes every key it holds. On MinIO, the mounted volume is what keeps the files: without it, a redeploy takes them with it.

Chapter 29 · PWA & Push

Installable app and notifications

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. 1

    Generate push keys

    Run the bundled command and copy the public and private pair into .env.

    bash
    pnpm push:keys
  2. 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.

    bash
    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
  3. 3

    Migrate existing subscriptions

    Stores upgrading from a release before native device push run the push migration once.

    bash
    pnpm db:migrate push --dry-run && pnpm db:migrate push
  4. 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. 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. 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.

Chapter 30 · Branding & Theme

Customize the storefront

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. 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. 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. 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. 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. 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.ts
    ts
    export 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.

Chapter 31 · Multi-language

Add or edit languages

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. 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. 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.ts
    ts
    export 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. 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. 4

    RTL support

    Arabic is included and switches layout direction automatically. Test menus, checkout, product pages, POS, and dashboards in RTL before launch.

  5. 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.

Chapter 32 · Admin & Roles

Add your team and set roles

The installer already created your Owner account. Use this chapter to add administrators and staff, run vendors, and get back into the admin if you are ever locked out.

  1. 1

    Add administrators and staff

    Give each person their own account on the Team page, and keep the Owner login for yourself. An administrator has full access to the admin. A staff member sees only what their permissions allow, for example POS, orders, products or the inbox.

    1. 1Open Admin → Team (https://your-domain/admin/staff) and click Add team member.
    2. 2Under Role, choose Administrator or Staff.
    3. 3Enter the Full name and Email.
    4. 4Staff only: under Permissions, pick a preset such as Cashier or Store manager, or tick permissions one by one. At least one is required. Give Inbox only to the people who answer customer conversations.
    5. 5Keep Send invite email ticked, then click Create team member.
    6. 6The person opens the invite email, sets a password and signs in at /login.
    The invite needs working email, so set up Email / SMTP first. The link expires after 1 hour. To send a new one, open the person in Admin → Team and click Send invite email.

    Check: The person appears in the Team list. After signing in, an administrator lands on /admin/dashboard and a staff member on /staff/dashboard.

  2. 2

    Scope staff to what they own

    A staff member can be limited to some vendors, POS locations or fulfillment regions. Every list, detail page and API route applies that scope, so a scoped staff member cannot open an out-of-scope order by typing its ID into the URL.

    1. 1Open the staff member in Admin → Team.
    2. 2In the Scope card, tick Vendors or Locations, or type Fulfillment regions (one per line).
    3. 3Leave the scope empty to give access to the whole store. Click Save changes.

    Check: Signed in as that staff member, the order list shows only orders inside the scope.

  3. 3

    Configure multi-vendor mode

    The installer's "Multi-vendor marketplace" box set this, and you can change it at any time. In single-store mode, Storify sells everything as the main store and the vendor area is closed.

    1. 1Open Admin → Settings → Multi-Vendor Mode (https://your-domain/admin/settings/marketplace).
    2. 2Turn the Multi-Vendor Mode switch on for a marketplace (vendor sign-up, vendor stores, vendor dashboard), or off for a single store.
    3. 3Click Confirm in the dialog, then save.

    Check: With the mode off, opening /vendor/dashboard sends you to the home page. With it on, approved vendors can use their dashboard there.

  4. 4

    Approve vendors

    In marketplace mode, sellers apply to open a store. You decide who may sell, which plan they are on and what commission you keep.

    1. 1Open Admin → Vendors. The Pending Review count shows new applications.
    2. 2Open a vendor to review its details, or open the row's menu and choose Approve vendor.
    3. 3Set up plans, commission and payouts as described in Vendors & Subscriptions.
    4. 4Registration rules and the onboarding steps are under Admin → Vendors → Configuration and Onboarding Flow.

    Check: The vendor's status shows Approved in Admin → Vendors.

  5. 5

    Regain access if you are locked out

    First try Forgot password? on /login; it works once email is set up. If it does not, reset the password with pnpm create-admin and your email. The command sets the new password, re-activates the account, makes it an administrator and signs it out everywhere.

    1. 1Where to run it depends on your platform. First Run & Installer, step "Locked out, or /install shows 404", shows how for your computer, a VPS, Dokploy and Vercel.
    2. 2Dokploy: in the container terminal, run cd /app && touch .env before the command.
    3. 3Vercel has no terminal: run the command on your own computer, with the live database settings in .env.
    4. 4Sign in at /login with the new password.
    bash
    pnpm create-admin you@your-domain.com "NewStrongPassword"
    Always type the new password in the command. Without it, the script uses ADMIN_PASSWORD from .env or the hosting panel (.env.example ships ADMIN_PASSWORD=change-me) and sets the password to that. Use a long password of letters and digits here, because the terminal can change symbols such as $ or !. You can change it later in your admin profile.

    Check: The command prints "Successfully upgraded … to Admin." and "Password updated.", and you can sign in.

Do not share admin accounts

Shared logins break accountability. Create a separate admin, staff or vendor account for every real person. The order timeline and the audit log record who did what.

Account status applies to everyone

Suspension and deactivation apply to every role, including admins and staff. The Owner cannot be suspended. Stores upgraded from a release before 1.4 run pnpm db:migrate account-status once; a new install needs nothing.

Chapter 33 · Security & Hardening

Lock the store down before launch

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.

Chapter 34 · What's New in 2.0

Release 2.0 — the changelog

2.0 is the largest release Storify has shipped. The storefront stops being a fixed layout and becomes a set of documents an admin arranges, the register keeps selling through a network outage, vendor access is derived from four explicit layers instead of one hand-held array, and the whole codebase went through a six-phase performance audit. This chapter is the release summary and the breaking changes; the itemised list of every feature, improvement and fix lives on the Changelog tab above.

Upgrading, not installing

Run the migrations before you read anything else

A fresh install needs none of this — the setup wizard handles it. An existing store crossing into 2.0 has eleven migrations to run, one of which silently locks vendors out if it is skipped, and a Node floor that moved. Every migration now runs through a single command with a dry pass that writes nothing, so start with --list and --dry-run, and read the Updates & Migrations chapter before you replace a single file.

  • Node 22.12.0 is the new minimum — pnpm install refuses to proceed below it, and on cPanel or a shared host this is the most common upgrade blocker.
  • db:migrate retire-vendor-perms is required on any marketplace: skipping it takes real access away from vendors who hold a retired permission.
  • The 86 db:migrate:<name> package scripts are gone, replaced by one pnpm db:migrate runner — update any deploy script that called the old names.
  • db:migrate refund-allocations runs after the new code is deployed, then the ledger is re-posted behind it.
Start here
bash
pnpm db:migrate --list              # every migration, grouped by release
pnpm db:migrate --all --dry-run     # writes nothing, prints what it would do
Go to Updates & Migrations

Theme engine and block builder

The home page, product page, listings, category, collection and cart are all built from schema-driven sections a merchant arranges in a visual builder — with drafts, an atomic publish, an embedded live preview with click-to-select, a saved-sections library and restorable version history. The built-in design renders until the first publish, so nothing has to be migrated to keep working.

A second storefront template

Electronics ships as a complete second design — its own home, product page, listings, showcase hero, dark category rail, deals panel and artwork — alongside the Classic template. A themes gallery shows real screenshots, and activating one asks whether to keep your layout or load the theme's starter into the home draft.

Header Studio and the Navigation hub

The header is now a row/column layout studio with five presets, a transparent glass mode, a category scope inside the search pill and a preview that plates the dark header from your own dark scheme. Footer link columns source their links from a menu instead of being hand-entered, and the mega menu builder draws the flyout at the shape the storefront renders it.

Product card studio and catalog policy

The card's badges, arrangement and out-of-stock treatment are designed once in Admin → Online Store → Product card and rendered from that configuration everywhere. Sold-out products can stay in place, move to the end, or be hidden — the default is exactly what your storefront did before the setting existed.

Offline point of sale

A register keeps selling through a network outage: sales queue behind an idempotency key, scanning and search fall back to a local catalogue snapshot, a provisional receipt prints, the terminal survives a reload, and the queue drains on reconnect. A replay that arrives after the stock has gone still commits, and reports the shortfall as an alert naming the products to recount.

Vendor access, rebuilt in four layers

Marketplace policy, plan entitlement, per-vendor grants and lifecycle, with capability packs grouping 48 permissions into 11 human decisions. Overrides carry an author and a reason, a locked page names the layer that denied it, and access requests land in an admin queue.

Admin Team, Owner and staff ownership

A store can have several administrators and exactly one protected Owner that cannot be demoted, suspended or removed. Staff now carry an explicit managedBy line, so scoping a platform staff member's data access to a vendor no longer hands that person to the vendor's dashboard.

Guest checkout leaves a customer record

Guest orders now create an email-keyed customer profile the Shopify way, claimed when that email later signs in, with a Guest tab in the admin customer list. Requires the guest-customers migration — the old unique index collides on the second guest row.

Returns, refunds and per-consignment allocation

A store return policy with delivery refunds by fault, restocking and return-shipping fees; a refund estimate a shopper sees before submitting; and every refund now recording which consignment it reversed, so payouts and commission invoices deduct it from the vendor it names instead of spreading it across the order.

Carrier scans everywhere, refunds reconciled everywhere

Parcel scans appear on the signed-in order page, in the shipped email and in the admin and vendor parcel lists, with failed deliveries quoting the courier's own words. Stripe, Paystack, Razorpay and a new PayPal webhook route now reconcile refunds pressed in a provider dashboard back to the order, the books and the payout.

Orange Money and MTN MoMo

Two mobile-money gateways join the payment lineup with selectable settlement currencies and their own reconciliation jobs. Both add a cron entry — /api/cron/orange-money-reconcile and /api/cron/mtn-momo-reconcile, every 15 minutes.

Price on request, and deposits paid off

A product can be sold by quote instead of by price: the storefront collects a quote request, the price stays out of the page and out of its structured data, and POS excludes it outright. Deposit pre-orders can now have their balance paid by the shopper rather than only invoiced.

“Deliver to” is a delivery address, not a filter

The header's shopper location now holds where the shopper is — read from the browser and named through OpenStreetMap, so it reads like a place — and follows them into checkout. What a listing is narrowed to lives only in that listing's URL, so a shopper who once picked a city no longer carries a 40 km slice of the store onto every page.

About Us, blog reading column, and a cart that adds up

A structured About Us page at /about with live numbers counted from real sellers, products and delivered orders; a proper reading column on blog articles; and a quantity stepper in the cart drawer. About Us ships visible with default copy — switch it off in its editor if you are not ready.

A free trial belongs to a paid plan

Trial days moved from free plans, where they meant nothing, to paid plans. A trial vendor is never asked for a payment method, the clock starts at approval rather than at application, and a lapsed trial closes the store into the ordinary “complete your subscription payment” screen with plan, products and orders untouched.

An install wizard that then disappears

A new store is stood up in five steps — system check, admin account, store basics, media storage with a live connection test, and template choice. It locks the moment an admin exists: the proxy stops redirecting and both /install and every wizard API answer 404 rather than 403.

A six-phase performance audit

Storefront first-load JavaScript fell from 2,450 KB to about 1,590 KB and the admin product editor from 2,701 KB to 1,859 KB; a warm storefront page now issues zero database queries; import cycles went from ten to one; and roughly 22 dead files, 563 unused exports and 6 packages were removed from the source buyers receive.

Faster, lighter, quieter builds

The production build dropped from 59 s to 31 s and .next/server from 327 MB to 137 MB. Fonts are self-hosted, so the build no longer reaches out to Google Fonts. Server source maps are off by default (BUILD_SOURCE_MAPS=true brings them back), and the build no longer prerenders every page for all 18 locales.

Polling replaced server-sent events

Both SSE streams are gone. Live data now refreshes on visibility, focus, reconnect and web push with conditional requests, so a hidden tab stops entirely instead of billing a serverless host for an idle dashboard.

One migration runner

The 86 db:migrate:<name> and :dry package scripts collapsed into a single pnpm db:migrate over one registry, with --list, a uniform --dry-run on every migration, and --all --yes to run the whole set in documented upgrade order.

Node 22.12.0 is the floor

1.3 through 1.5 ran on Node 20.19.28. From 2.0 the minimum is 22.12.0 and pnpm install refuses below it. Check the Node version your host actually runs before you upload anything — on cPanel and shared hosting this is the single most common upgrade blocker.

The per-migration package scripts were removed

pnpm db:migrate ledger, pnpm db:migrate boosts --dry-run and the other 84 no longer exist. Any deploy script, runbook or CI job that calls them will fail with an unknown-script error. The replacement is pnpm db:migrate <name> and pnpm db:migrate <name> --dry-run.

Your storefront is not rearranged by upgrading

db:migrate store-pages publishes your existing settings.homePage through the same mapping the storefront's fallback uses, so the page comes out looking exactly as it did — it is simply editable in the builder now. Every other surface renders its built-in design until you publish something over it, and settings.homePage is deliberately left in place as the rollback path.

Nothing here changes a fresh install

New stores go through the setup wizard, which stamps the current shape directly. This chapter and the migration list below only concern a database that already holds orders.

Chapter 35 · Updates & Migrations

Upgrade to 2.0 without losing a figure

Upgrading an existing, live installation to 2.0. Read the notes for every version you are skipping, in order — 1.3 to 2.0 means running 1.4's migrations, then 1.5's, then 2.0's. Nothing here is optional to read; plenty of it is optional to run, and each list says which.

Before anything else

Back up, check Node, then dry-run the whole migration set

Every migration is written to be re-runnable and most refuse to double-apply, but a backup is the only thing that turns a bad upgrade into a five-minute problem. Take the dump, confirm the Node floor, and run the dry pass — it writes nothing and tells you which migrations your store actually needs before you have touched a file.

  • If you still keep files on local disk, copy public/uploads and private-uploads out of the app folder too — an update method that replaces the folder deletes digital goods customers have already paid for.
  • A dry run that reports 0 is a migration you can skip: several only matter to stores that used a particular feature.
The three commands to run first
bash
node -v                                   # must be 22.12.0 or newer
mongodump --uri="$MONGODB_URI" --out=./backup-$(date +%Y%m%d)
pnpm db:migrate --all --dry-run           # reports; writes nothing
  1. 1

    Back up the database, and the media if it is still local

    Take the dump before you merge anything. If you are on local disk storage, copy public/uploads and private-uploads somewhere outside the app folder — they hold digital deliverables, and a zip overwrite or a container with no volume deletes them.

    bash
    mongodump --uri="$MONGODB_URI" --out=./backup-$(date +%Y%m%d)
    cp -r public/uploads private-uploads ~/storify-media-backup/
  2. 2

    Confirm the Node version

    The floor moved in 2.0: releases 1.3 to 1.5 ran on Node 20.19.28, and 2.0 requires 22.12.0. pnpm install refuses to proceed on anything older. Check this on the machine that will actually run the build, not only on your laptop.

    bash
    node -v   # v22.12.0 or newer
  3. 3

    Download the build and diff your environment file

    Download the update from your CodeCanyon account and read the changelog for every version between yours and this one. Do not overwrite .env with the new .env.example — diff the two and copy across what is new. Anything missing is almost always an integration you do not use; the app starts without it and that feature stays off. 2.0 itself adds no required variables.

    bash
    diff <(grep -oE '^[A-Z_0-9]+' .env.example | sort -u) \
         <(grep -oE '^[A-Z_0-9]+' .env | sort -u)
  4. 4

    Put the store in maintenance mode and replace the source

    Switch maintenance mode on under Admin → Settings → Maintenance (Enable Maintenance Mode → Save Maintenance Settings), then replace the source while keeping .env, public/uploads/ and private-uploads/ in place.

  5. 5

    Install and build before you migrate, not after

    Two migrations — the refund and ledger backfills — are documented to run once the new code is deployed, so that nothing behind them is still writing rows in the old shape. Type checking no longer runs inside next build, so run it separately if you have modified the source.

    bash
    pnpm install
    pnpm typecheck
    pnpm test
    pnpm build
  6. 6

    One command runs every migration

    The 86 per-migration package scripts are gone. A single runner reads one registry, so every migration has the same interface and the same uniform --dry-run — including the four whose underlying scripts predate the convention. --list is the fastest way to see what a migration does and whether your store needs it.

    The runner
    bash
    pnpm db:migrate --list                     # all of them, grouped by release
    pnpm db:migrate vendor-access --dry-run    # report what it would do, write nothing
    pnpm db:migrate vendor-access              # apply it
    pnpm db:migrate --all --dry-run            # dry-run the whole set, in upgrade order
    pnpm db:migrate --all --yes                # then apply, once you have a backup
    
    # Unrecognised flags pass through to the migration itself
    pnpm db:migrate location-geo --geocode
    pnpm db:migrate ledger -- --rebuild
  7. 7

    Run the 1.3 → 1.4 migrations

    Sixteen migrations. This release moved shipping geography, vendor locations and inventory onto their current model, hardened authentication, and landed omnichannel messaging. reconcile-stock, barcodes and vendor-billing are left out of --all: each needs its dry pass read and acted on first.

    1.4
    bash
    pnpm db:migrate audit-indexes          # also resets audit retention from 90 days to 24 months
    pnpm db:migrate vendor-zone-rates      # vendors move onto the store's shipping zones
    pnpm db:migrate location-vendor        # every inventory location gains an owning vendor
    pnpm db:migrate drop-pickup-slots      # retire the old booked-slot pickup model
    pnpm db:migrate vendor-geo             # radius search can see legacy { lat, lng } vendors
    pnpm db:migrate location-geo --geocode # a point on every collection branch (calls an API)
    pnpm db:migrate account-status         # clears stale inactive status — skipping risks a lockout
    pnpm db:migrate push
    pnpm db:migrate omnichannel
    pnpm db:migrate telegram-keys          # only if Telegram was connected before 1.4
    pnpm db:migrate support-conversations  # only if you used the legacy support tickets
    
    # Not part of --all. Read each dry pass and act on it before applying.
    pnpm db:migrate reconcile-stock --dry-run  # the apply also needs --source: which side wins
    pnpm db:migrate barcodes --dry-run         # resolve the duplicates it names, then re-run
    pnpm db:migrate vendor-billing --dry-run   # backfills subscription snapshots from Stripe
  8. 8

    Run the 1.4 → 1.5 migrations

    Sixteen more, and the largest jump in the product's history: boosting, the finance ledger, carrier shipping, guest customer records and the retirement of local file storage. Run the ones for features you use — except storage, which every store still on local disk needs. Without the ledger backfill, every finance report starts on the day you upgraded.

    1.5
    bash
    # Required for everyone
    pnpm db:migrate guest-customers       # rebuilds customerprofiles.userId_1 as a partial index
    pnpm db:migrate storage-credentials   # per-provider credential blocks
    pnpm db:migrate carrier-shipments
    pnpm db:migrate webhook-provider      # multi-provider events, plus a retention window
    
    # Multi-vendor
    pnpm db:migrate suborder-carrier
    pnpm db:migrate suborder-payment
    pnpm db:migrate commission-source
    
    # Boosting
    pnpm db:migrate boosts
    pnpm db:migrate boost-permissions     # without it, existing vendors cannot see the screens
    pnpm db:migrate drop-boost-packages
    
    # Housekeeping and optional
    pnpm db:migrate drop-stocks-inventory
    pnpm db:migrate subscription-currency # repairs plan snapshots stamped with a placeholder
    
    # Configure storage first, then move the files
    pnpm db:migrate media-to-cloud
    
    # Read the dry pass first
    pnpm db:migrate loyalty --dry-run     # grants points; wrong on a store that has none
    pnpm db:migrate debrand --dry-run     # only if you seeded demo data
  9. 9

    Run the 1.5 → 2.0 migrations

    Eleven migrations, no new environment variables. store-pages and retire-vendor-perms are the two that must run; the notification and query indexes are only needed if you set MONGODB_AUTO_INDEX=false, since Mongoose creates them on first boot otherwise. Leave refund-allocations for the next step.

    2.0
    bash
    # Required
    pnpm db:migrate store-pages --dry-run  # prints the target host/db and every write
    pnpm db:migrate store-pages            # publishes settings.homePage as a real document
    pnpm db:migrate retire-vendor-perms    # required on any marketplace — see the warning below
    
    # Recommended: write the current state down instead of leaving it inferred
    pnpm db:migrate staff-ownership        # stamps managedBy on every staff profile
    pnpm db:migrate team-roles             # legacy seller -> staff, and designates the store Owner
    pnpm db:migrate vendor-access          # legacy permissions array -> entitlements + overrides
    pnpm db:migrate pack-policy            # eight policy booleans -> one switch per capability pack
    pnpm db:migrate vendor-owner-roles     # realigns owner roles; never demotes
    
    # If you take card or mobile-money payments
    pnpm db:migrate charge-fees --dry-run
    pnpm db:migrate charge-fees            # dashboard net stops equalling gross
    
    # Only if you set MONGODB_AUTO_INDEX=false
    pnpm db:migrate notifications
    pnpm db:migrate phase3-indexes
  10. 10

    After deploying: repair historic refunds, then re-post the ledger

    refund-allocations records what each historic refund reversed, per consignment. Run it once the 2.0 code is live so nothing behind it is still writing rows in the old shape, then re-post the ledger so the books follow the repaired rows. It writes only the allocation — it never moves money or changes an amount — and on most stores it reports 0, because only mixed orders were ever wrong.

    bash
    pnpm db:migrate refund-allocations --dry-run
    pnpm db:migrate refund-allocations
    pnpm db:migrate ledger                # re-post the repaired rows
  11. 11

    Add the two mobile-money cron entries

    2.0 adds Orange Money and MTN MoMo, each with a reconciliation job. The bundled vercel.json already lists all ten routes; if you schedule crons yourself, add these two. CRON_SECRET is not optional once you use them — without it the endpoint answers 401 and simply never runs.

    json
    { "path": "/api/cron/orange-money-reconcile", "schedule": "*/15 * * * *" },
    { "path": "/api/cron/mtn-momo-reconcile",     "schedule": "*/15 * * * *" }
  12. 12

    Re-check the settings 2.0 introduced

    Open Admin → Team and confirm the Owner is the account you expect. Check Vendors → any vendor → Access, that nobody lost a screen. Open Online Store → Menus → Header once if you had customised the old header builder — a store that never opens the studio renders the classic preset. Then look at Online Store → Product card for the out-of-stock rule, the About Us page under Pages, Deliver to under the header's shopper location, and trial days on any paid vendor plan.

  13. 13

    Verify the state rather than trusting the output

    Confirm Finance → Overview covers the whole trading history rather than starting at the upgrade, that Online Store → Customize renders your storefront, and that no vendor has lost access. Then place one test order end to end, including a refund if you use them. The storefront picks section changes up inside a 60-second revalidate window — give it a minute before deciding something did not work.

    bash
    pnpm typecheck && pnpm lint && pnpm test
  14. 14

    Schedule database backups

    Use MongoDB Atlas automated backups, or 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
  15. 15

    Back up uploaded media

    Enable bucket lifecycle and versioning where the provider supports it, and keep the credentials safe. On MinIO, the mounted volume is the thing to back up — it holds digital deliverables as well as images.

retire-vendor-perms is not safe to skip

Eleven decorative vendor permissions are gone from the code, and they were not inert: the implication table let one of them satisfy a real guard, so holding create_pos made an access_pos check pass. A vendor who holds the verb but not its parent loses real access the moment the code drops it. This migration promotes every such holding to its parent first — which is why it cannot be a plain $pull, and why skipping it silently locks vendors out.

Mongo will not change an existing TTL index

Mongoose creates indexes on first boot, but it only ever creates — it never drops, and MongoDB will not alter expireAfterSeconds on an index that already exists. The audit retention change is the clearest example: without db:migrate audit-indexes, an upgraded database keeps expiring order history at 90 days no matter what the new code says. On a large store, set MONGODB_AUTO_INDEX=false and manage index changes through these scripts alone.

Copy your media out before you deploy

If your update method replaces the app folder — a cPanel upload, a zip overwrite, a container with no volume — it deletes public/uploads and private-uploads with it. Copy them somewhere else first, then restore and run pnpm db:migrate media-to-cloud once a provider is configured.

A migration that reports duplicates has not failed

The barcode and carrier-shipment migrations scan for conflicting rows before building a unique index. If any exist, the index build is skipped and reported rather than failing — resolve the duplicates it names, then re-run. For carrier shipments, keep the newest of each set; the older ones are abandoned drafts.

pack-policy may print an orders split warning

Orders used to sit under two booleans — one for viewing the list and one for everything else — which made a marketplace-wide “vendors may look but not touch” possible. One switch per pack cannot express that, so where the two disagree the migration reports it rather than resolving it silently. If you see the warning, set that store's Orders policy by hand afterwards.

Rolling back means restoring both

Restore the previous source and the database dump together. Several migrations change the shape of data the old code cannot read — retired permissions, per-provider storage credentials, sub-order carrier and payment fields — so old code against a migrated database is not a supported combination.

Chapter 36 · Troubleshooting

Fix common issues

Most problems have a known cause. The first cards cover errors during installation and deployment, and most of their titles are the exact message you see. The cards after them cover problems in a running store. Check these before you open a support ticket.

Start here

Find the exact error message first

Most card titles below are the exact words of an error. Copy a few words of your message and search this page with your browser (Ctrl+F, or Cmd+F on a Mac). The message is usually in one of these places:

  • Your computer: the terminal window where pnpm install, pnpm dev, pnpm build or pnpm start runs.
  • Vercel: open the deployment to read its build log. For errors while the store runs, open the project's Logs page.
  • Dokploy: the application's Deployments tab has the build log. The Logs tab shows the running store.
  • VPS: run sudo journalctl -u storify -n 100 --no-pager to see the last 100 lines of the store's log.
  • The browser: the message on the page, or the rows of the installer's System check at /install.

ERR_PNPM_UNSUPPORTED_ENGINE

pnpm stopped because its own version, or your Node.js version, does not match what Storify needs. The Expected version and Got lines tell you which. On Vercel: add ENABLE_EXPERIMENTAL_COREPACK=1 under Settings → Environment Variables, then Deployments → ⋯ → Redeploy. On your computer: install Node.js 24 LTS, run corepack enable, and check that pnpm -v prints 10.24.0. On Node 25 or newer, run npm install -g corepack first. On Windows, if corepack enable reports a permission error, run the terminal as Administrator.

ERR_PNPM_LOCKFILE_CONFIG_MISMATCH

The message says Cannot proceed with the frozen installation and names overrides or pnpmfileChecksum. Hidden files were lost when the code was copied: pnpm-workspace.yaml or .pnpmfile.cjs is missing next to package.json. Show hidden files (Windows Explorer: View → Show → Hidden items; macOS Finder: Cmd+Shift+.), copy both files from the zip you downloaded into the app folder, and run the install again. If you deploy from GitHub, commit and push them. On Dokploy, also check that the Build Type is Nixpacks, not Railpack.

pnpm install fails

First check the hidden files: pnpm-workspace.yaml and .pnpmfile.cjs must sit next to package.json (see the ERR_PNPM_LOCKFILE_CONFIG_MISMATCH card). Otherwise delete only the node_modules folder, keep pnpm-lock.yaml, run corepack enable, then run pnpm install again. Use pnpm only: npm and yarn do not read this lockfile.

'NODE_OPTIONS' is not recognized as an internal or external command

You ran pnpm build in Windows Command Prompt or PowerShell, and the build script uses Linux-style syntax. Run pnpm config set shell-emulator true once, then run pnpm build again. WSL2 also works. pnpm dev works without either.

This script must be run as root

The Dokploy install script needs the root user. Run sudo -i, then run curl -sSL https://dokploy.com/install.sh | sh again. See Deploy with Dokploy.

Error: something is already running on port 80

The same message can name port 443 or 3000. Dokploy needs all three ports free, and another web server (nginx, Apache, Caddy) or app already uses one of them. Install Dokploy on a fresh Ubuntu server with nothing else on it. Do not install it on a server you already set up with Deploy to a VPS.

Could not resolve host: github.com (Dokploy build)

On some VPS providers, such as Hetzner, Docker builds cannot use the provider's DNS servers. Give Docker public DNS servers. First run ls /etc/docker/daemon.json. Only if it answers No such file or directory, run sudo mkdir -p /etc/docker && echo '{"dns": ["1.1.1.1", "8.8.8.8"]}' | sudo tee /etc/docker/daemon.json && sudo systemctl restart docker, then deploy again. If the file exists, add the dns entry to it instead. Restarting Docker briefly restarts every container, including Dokploy.

Hobby accounts are limited to daily cron jobs. This cron expression would run more than once per day.

Vercel's free Hobby plan only runs jobs once a day, and Storify's vercel.json has jobs that run every minute, so the deploy fails. A live store needs Vercel Pro, because Hobby is for non-commercial use only: upgrade the team (or start the 14-day Pro trial), then Redeploy. For a private test on Hobby only: delete the whole "crons" block from vercel.json, commit and push. Background jobs then do not run. See Deploy to Vercel.

MongoDB connection error … ENOTFOUND in the build log

This is expected, and harmless, when the build cannot see the database, for example a Dokploy MongoDB service whose internal address only works for the running store. The build still finishes, and lines such as [sitemap] … emitting hub pages only follow. The sitemap fills in once the running store reaches the database. It is a real problem only if the build fails, or if the running store logs the same error: then the host in MONGODB_URI is wrong.

The site shows an error instead of the installer

When the store cannot reach MongoDB, pages do not redirect to the installer. Open https://your-domain/install directly. The MongoDB connection row shows the problem, and the other rows say "Not checked — the installer could not reach its database". Fix MONGODB_URI and MONGODB_DB_NAME, allow your server in the Atlas IP Access List (Vercel: 0.0.0.0/0), restart or redeploy, then click Check again.

bad auth : authentication failed

MongoDB Atlas refused the user name or password in MONGODB_URI. The log may show MongoServerError: bad auth : authentication failed (code 8000; the capital letters can differ). Check the database user in Atlas (Security → Database & Network Access; the exact labels may differ slightly). Make sure <db_password> was replaced by the real password, without the < and >. Percent-encode special characters in the password (@ → %40, : → %3A, / → %2F, # → %23, ? → %3F), or choose a password with letters and digits only.

Could not connect to any servers in your MongoDB Atlas cluster

Atlas does not accept connections from the address your store runs on. In Atlas, open Security → Database & Network Access → IP Access List → Add IP Address. Add your own IP (local install), your server's IP (Dokploy or VPS), or 0.0.0.0/0 for Vercel, whose addresses change. When the entry shows as active, restart or redeploy.

connect ECONNREFUSED 127.0.0.1:27017

Nothing answers at the database address in MONGODB_URI. On your computer, MongoDB is not running: start it again (Docker: docker start storify-mongo; Windows: start the MongoDB service in the Services app). If the message shows ::1:27017, write 127.0.0.1 instead of localhost in MONGODB_URI. For MongoDB on a server, check that it is running and reachable from the app on port 27017, and that the user and password are right. A Dokploy MongoDB service needs its Internal Connection URL with authSource=admin (see Deploy with Dokploy).

Missing database name. Set MONGODB_DB_NAME, or include it in MONGODB_URI

pnpm create-admin or pnpm db:seed:users could not tell which database to use. Add MONGODB_DB_NAME=storify to the .env the command reads. If your store already ran on Atlas without this variable, its data is in a database called test: set MONGODB_DB_NAME=test for the command instead, so it works on the same data.

Insecure auth configuration (BETTER_AUTH_SECRET)

The build is fine; the running store refuses to sign anyone in. The log shows [storify] Insecure auth configuration: BETTER_AUTH_SECRET is not set… (or "…is only N characters" or "…is still the example placeholder"), and the installer's Authentication secret row shows a yellow warning triangle. The message suggests openssl, which Windows does not have; this command works everywhere: node -e "console.log(require('crypto').randomBytes(32).toString('hex'))". Save the 64 characters it prints as BETTER_AUTH_SECRET (exactly this name). Your computer: edit .env, press Ctrl+C in the terminal and start the store again. VPS: edit .env, then run sudo systemctl restart storify. Vercel: Settings → Environment Variables, then Redeploy. Dokploy: Environment tab → Save → Deploy.

Invalid origin (sign-in fails)

Sign-in answers 403 "Invalid origin" because you opened the store on an address that is not the one in BETTER_AUTH_URL and NEXT_PUBLIC_APP_URL: for example a *.vercel.app preview link, an old domain, the www version of the address, or http:// instead of https://. Open the store on the exact address in those variables. To use a new address, set both variables to it (https, no / at the end), then rebuild: on Vercel Redeploy without the build cache, on Dokploy click Deploy, on a VPS run pnpm build and restart. The installer's Application URL row warns about this too.

/install shows 404 Not Found

The installer closes for good once an admin account exists: after you finished it, or after pnpm create-admin, pnpm db:seed or pnpm db:seed:users. Sign in at /login instead. If you do not know the password, use Forgot password? (needs email) or follow "Locked out, or /install shows 404" in First Run & Installer. Only on a test database you can throw away: pnpm db:reset deletes all data, and the installer comes back after you restart the app.

node: .env: not found

Commands such as pnpm create-admin and pnpm db:migrate need a .env file in the app folder, even an empty one. In the Dokploy terminal, run cd /app && touch .env, then run your command again; the Environment tab values still apply. On your own computer, run the command inside the app folder (the one with package.json) and create the .env file there first.

Bad Gateway on Dokploy

Dokploy's web server (Traefik) cannot reach the store. Check in this order: the Deployments tab shows the last deploy finished without errors (right after a deploy, wait a minute for the store to start); on the Domains tab the Container Port is 3000; the Logs tab shows no crash; Advanced → Run Command is empty; you did not set PORT to another value. If the server froze during the build, add swap and the build limits from Deploy with Dokploy.

Background jobs have stopped (dashboard notification)

The dashboard found jobs that have not run recently ("Not running: …") or that failed three times in a row ("Running but failing every time: …"). Right after install this is expected: every job counts as not running until its first run. After an hour, a new notice should name at most vendor-subscriptions, gateway-disputes, finance and preorders, and after a day none. If messaging-outbox or carrier-shipments are still named after an hour, the schedule is not running: on Dokploy or a VPS, add the schedules from Scheduled Jobs; on Vercel, open Settings → Cron Jobs → View Logs. A 401 means CRON_SECRET is missing or different where the job is called, and a 401 run is not counted, so the job keeps looking stopped. For a job that keeps failing, read its log.

File is too large to upload through the server

The browser could not send the file straight to your storage bucket, so it went through the server, which refused it (on Vercel the limit is 4.5 MB). Usually the bucket has no CORS rule for your domain. Add the rule from Storage & Media and allow PUT, GET and HEAD from your exact store address (add the www. address too if you use it). Then click Test Connection in Admin → Settings → Storage and upload again.

AI features unavailable

Check the key in Admin → Settings → AI Configuration (or OPENAI_API_KEY), run the connection test, and confirm that the specific feature, 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 set in the same test or live mode as your keys, and that the endpoint is public and needs no login. On Vercel, use the production domain, not a preview link. The URLs are listed in the Launch Checklist, step "Point webhooks at the live domain".

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 every hour 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. On Dokploy or a VPS, type the webhook URL yourself (https://your-domain/api/webhooks/meta): the admin screen may show a localhost address.

Outbound messages or emails stall

Both retry through background jobs. Confirm that messaging-outbox, messaging-escalations and email-deliveries are scheduled and return 200, not 401 from a wrong CRON_SECRET. See Scheduled Jobs.

Emails landing in spam

Set up SPF, DKIM and DMARC for your sender domain. Resend and SendGrid have step-by-step screens for this.

Product images or 3D models do not show

Check the storage provider settings, the Public URL, bucket permissions and the file type. After switching providers, make sure the new Public URL was saved and not left as an unsaved change. Test Connection in Admin → Settings → Storage checks the Public URL for you.

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 total.

Vendor products missing from POS

Each register sells only its own catalog. The admin and staff register sells the main store's products, and only those with the Point of Sale channel ticked on the product. A vendor sells their own products from their own POS; that needs multi-vendor mode on and Vendors turned on under Admin → Settings → Point of Sale (POS) Access, in the POS Access by Role card.

A country is missing at checkout

Admin → Settings → General → Available countries controls this. If it is set to selected countries only, just 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. Fix each duplicate product or variant barcode in the catalog, then run the dry run again until it is clean before you apply the migration.

Shiprocket stays greyed out with the origin set to India

The origin field stores a country name, and the check looks it up in the country list, so both forms are accepted. If it is still refused, the origin is really elsewhere or not set. Shiprocket only ships from India.

A wall of near-identical Shippo refusals

Shippo answers a no-rate shipment once per carrier account. Storify groups those answers into one summary in the Send to courier dialog. If it names the origin, your store ships from outside the coverage of Shippo's default accounts: connect a carrier account of your own.

A carrier reports rejected credentials

The token or login was revoked or changed. Paste the new one and click Test connection. The warning clears itself, and the cached Shiprocket token is dropped at the same moment. The carrier is only flagged, never disabled, so a one-off 401 does not switch off your shipping.

Images return 403 while the credentials test fine

The bucket is not publicly readable, or the Public URL is missing and requests fall back to the API endpoint, which never allows anonymous reads. Test Connection catches this: it uploads a probe file and fetches it back over the Public URL.

Finance reports start on the day I upgraded

On an existing store the ledger starts empty. Run pnpm db:migrate ledger to replay paid orders, refunds, payouts and labels. If a figure still looks wrong after a posting rule changed, rebuild rather than re-run: a plain re-run collides with the old entry, and the old entry stays.

Vendors get forbidden on Boosts

Vendor permissions are saved on each vendor when it is created, so existing vendors do not get new permissions from a code update. Run pnpm db:migrate boost-permissions.

Deletes refused with a demo message

Demo mode is on: DEMO_MODE or NEXT_PUBLIC_DEMO_MODE is set to true, 1, yes or on. The message "Demo mode is enabled. Deletes, settings edits, and test actions are disabled…" is the server's own refusal, not a random failure. On a real store, delete both variables and restart. Rebuild if you had set NEXT_PUBLIC_DEMO_MODE.

Before you open a support ticket

Have these ready: the Storify version (from the zip name), where you install it (your computer, Vercel, Dokploy or a VPS), the exact error message, the last lines of the log, and what you already tried. Never send your .env values, keys or passwords.

Updates·Last updated September 25, 2026
↑ Back to top