Game server / VPS / Discord bot hosting storefront with:
- Automated Pterodactyl provisioning — pay → set a panel login → server is created and allocated automatically.
- Stripe + PayPal, both wired up.
- A full admin CMS at
/admin— products, payment gateway keys, Pterodactyl connection, redirect links, and order management, all editable live with no redeploy.
1. Customer picks a plan on / (products come from the DB now, editable in /admin/products)
2. /checkout/[slug] -> creates an Order row (status: pending)
-> Stripe Checkout redirect, or PayPal Buttons popup
3. Payment happens on Stripe/PayPal's hosted page -- we never touch card data
4. Stripe webhook (or our PayPal capture call) flips Order.status -> "paid"
*** This is the only place that's ever allowed to mark an order paid ***
5. Customer lands on /provision/[orderId]
- page polls the order until status === "paid"
- shows a form: panel username, panel email, password
6. POST /api/provision
- re-checks order.status === "paid" server-side (never trusts the client)
- calls lib/pterodactyl.js:
findOrCreateUser() -> POST /api/application/users
createServer() -> POST /api/application/servers (auto-allocated
via Pterodactyl's `deploy` field, sized from
the plan you configured in /admin/products)
- marks Order.status -> "provisioned"
7. Customer sees "your server is live" + a link to the panel
| Page | What it controls |
|---|---|
/admin |
Dashboard -- order count, revenue, provisioning failures |
/admin/products |
Add/edit/delete plans: name, price, specs shown to customers, and the Pterodactyl egg/nest/location/resource mapping used at provision time |
/admin/infra |
Sync real node data from Pterodactyl (name, capacity, maintenance mode); set each node's map location since Pterodactyl doesn't do geography; manually or automatically check reachability |
/admin/orders |
Every order, its status, and a Retry button for anything stuck after a failed Pterodactyl call |
/admin/settings |
Site name, support email, panel URL, and redirect overrides |
/admin/settings/branding |
Logo, favicon, and the three theme colors (primary/accent/background) -- with a live preview |
/admin/settings/content |
Every piece of homepage copy: hero headline/subheadline/CTA, the 4 feature cards, the "how it works" steps, footer text |
/admin/settings/payments |
Turn Stripe/PayPal on or off and set their API keys |
/admin/settings/pterodactyl |
Panel URL + Application API key, with a Test connection button |
Everything on these pages is stored in the database (Setting and Plan
tables) and read fresh on every request -- editing a price, changing your
logo, rewriting the hero headline, or rotating your Pterodactyl API key
takes effect immediately, with no rebuild or redeploy.
Tailwind's color tokens (violet, cyan, void -- see
tailwind.config.js) don't point at fixed hex values; they point at CSS
variables (--brand-primary, --brand-accent, --brand-bg). app/layout.js
reads your saved colors from the database on every request and injects
them as an inline <style> override. That means every bg-violet /
text-cyan / bg-void class already used throughout the app repaints to
match whatever you set in Admin → Branding, without anyone having to
hunt down and edit each component.
Real node data, not decoration. /admin/infra → Sync from Pterodactyl
pulls every node's name, FQDN, total memory/disk capacity, and maintenance
mode straight from the Application API. Pterodactyl has no concept of
geography, so latitude/longitude/city/country are set once per node in the
admin panel; everything else re-syncs automatically on demand.
"Status" (online / maintenance / unreachable) is a real reachability
probe against each node's own Wings daemon — an HTTP request straight to
https://{fqdn}:{daemonPort}/api/system with a short timeout, run either
manually (Check now) or on a schedule via vercel.json's cron config
(/api/cron/check-infra, every 10 minutes by default, gated by
CRON_SECRET). There's no fabricated traffic/DDoS telemetry anywhere in
this page — Pterodactyl doesn't expose that data, so showing invented
numbers there would be misleading. If you want real network/security
metrics later, that has to come from your actual edge/DDoS provider's API
(Cloudflare, etc.) — happy to wire that in separately if you use one.
This is a single-tenant product: one deployment = one brand. Branding, products, and settings are global, not per-customer. If you want to resell this as a template, the intended workflow is "clone the repo, deploy one instance per client, each with their own admin login and branding" -- not one shared instance serving multiple independent storefronts. True multi-tenant SaaS (isolated brands/data under one deployment, with its own billing-for-the-platform layer) is a meaningfully larger project than this one; happy to scope that separately if you actually need it.
The one thing that has to live in an environment variable (not the admin panel) is the admin login itself -- you need a way in before the panel has anything configured. See Setup step 2 below.
npm install# Generates a bcrypt hash -- copy the output line into .env
npm run hash-password -- "your-strong-password"
# Generate a random signing secret for admin sessions
openssl rand -hex 32Put both into .env:
ADMIN_PASSWORD_HASH="$2a$10$..."
ADMIN_SESSION_SECRET="the openssl output"
cp .env.example .env # if you haven't already
npx prisma migrate dev --name init
npm run db:seed # optional: adds 4 starter plans you can editnpm run devVisit http://localhost:3000/admin/login and sign in.
Go to /admin/settings/pterodactyl, /admin/settings/payments, and
/admin/products and fill in your real values -- see the next section for
where each value comes from. You technically don't need to touch .env
again after step 2, other than DATABASE_URL.
Pterodactyl (Admin -> Settings -> Pterodactyl in this app) You said you don't have a panel yet -- you need one before provisioning can work (this Next.js app is the storefront; Pterodactyl is the hosting panel it calls). Rough path:
- Get a VPS (Ubuntu 22.04 is the safest choice).
- Install the Panel: https://pterodactyl.io/panel/1.0/getting_started.html
- Install Wings (runs the actual game server containers) and link it to the panel.
- In the panel: create a Location, a Node, and give the node an
allocation range (IP:port pool) -- this is what
deployinlib/pterodactyl.jsauto-picks from. - Under Nests, note the egg IDs you want to sell.
- Admin -> Application API -> Create New -> paste the URL + key into
this app's
/admin/settings/pterodactyl, then hit Test connection. - In
/admin/products, set each plan's Nest ID / Egg ID / Location ID / Docker image / resources to match.
Stripe (Admin -> Settings -> Payments)
- dashboard.stripe.com -> Developers -> API keys -> copy publishable + secret key.
- Developers -> Webhooks -> Add endpoint ->
https://yourdomain.com/api/webhooks/stripe, listening forcheckout.session.completed(and optionallycheckout.session.expired,payment_intent.payment_failed). Copy the signing secret. - Paste all three into
/admin/settings/paymentsand toggle Enabled. - Local testing:
stripe listen --forward-to localhost:3000/api/webhooks/stripe
PayPal (Admin -> Settings -> Payments)
- developer.paypal.com -> My Apps & Credentials -> Create App.
- Paste Client ID + Secret into
/admin/settings/payments, leave environment on Sandbox until you're ready to go live, toggle Enabled.
Vercel's filesystem is ephemeral, so SQLite won't persist. Use
Neon (neon.tech), Supabase (supabase.com), or Vercel Postgres -- any of
them give you a DATABASE_URL.
Then change prisma/schema.prisma:
datasource db {
- provider = "sqlite"
+ provider = "postgresql"
url = env("DATABASE_URL")
}- Push this project to a GitHub repo.
- vercel.com -> New Project -> import the repo. Next.js is auto-detected.
Project -> Settings -> Environment Variables. At minimum:
DATABASE_URL=postgres://...
ADMIN_PASSWORD_HASH=...
ADMIN_SESSION_SECRET=...
(Everything else -- Stripe keys, PayPal keys, Pterodactyl URL/key -- you'll
enter through /admin after the first deploy, since those are DB-backed
now. You can also pre-fill them as env vars if you prefer; they're used as
a one-time seed on first read.)
Vercel's build step doesn't run interactive Prisma commands for you, so do
this once, locally, pointed at the production DATABASE_URL:
DATABASE_URL="your-production-url" npx prisma migrate deploy
DATABASE_URL="your-production-url" npm run db:seed # optionalRun prisma migrate deploy again any time you change prisma/schema.prisma
and redeploy.
Trigger a deploy (push to main, or click Deploy in the dashboard).
- Stripe webhook endpoint ->
https://yourdomain.com/api/webhooks/stripe - Log in at
https://yourdomain.com/admin/loginand fill in Pterodactyl + payment settings if you didn't pre-seed them via env vars.
Pterodactyl itself (the panel + Wings, which run Docker containers for every game server) cannot run on Vercel -- it needs a real VPS/dedicated box you control. Vercel only hosts this Next.js storefront, which makes plain HTTPS API calls to your Pterodactyl panel wherever it lives. That's fine and normal -- just make sure your panel's URL is publicly reachable over HTTPS so Vercel's serverless functions can reach it.
- Stripe/PayPal secret keys and the Pterodactyl API key are only ever read
server-side (
lib/,app/api/**/route.js) -- never sent to the browser. The admin settings screen shows them masked after saving and never round-trips the real value back into the input unless you type a new one. - Orders only ever move to
"paid"from the Stripe webhook or a server-side PayPal capture call -- never a client redirect. /api/provisionre-checksorder.status === "paid"itself and is idempotent (safe to call twice without creating two servers)./admin/**and/api/admin/**are gated bymiddleware.js, checking a signed, expiring session cookie (HMAC via Web Crypto, valid on both the Edge middleware runtime and Node API routes) before anything loads.- Panel passwords are hashed (
bcryptjs) before being stored in this app's own database; the plaintext is only ever used in the one server-to-server call to Pterodactyl (which hashes it again on its own side).
- Email delivery (Resend/SendGrid) for receipts and the "your server is ready" notification, so customers aren't relying on staying on the tab.
- Recurring billing -- this charges once per order; real monthly hosting normally uses Stripe Subscriptions plus a renewal-failure -> suspend job against the Pterodactyl API.
- Multi-admin accounts / roles, if more than one person needs CMS access (current design is intentionally single-operator).
- Rate limiting on checkout/provision endpoints.
If you'd rather not maintain custom code long-term, Paymenter (paymenter.org, MIT-licensed) is an open-source billing platform with a first-party Pterodactyl integration that does the same buy -> provision flow plus recurring billing and suspensions out of the box. This project is worth it if you want a fully custom storefront and are comfortable extending the code yourself; Paymenter is worth it if you want to be live faster with less maintenance.