Skip to content

Repository files navigation

ReturnPilot

E-commerce Returns & Warranty Management SaaS -- Next.js (App Router) + TypeScript + PostgreSQL/Prisma.

Stack

  • Next.js 14+ (App Router), TypeScript
  • PostgreSQL via Prisma ORM (Neon free tier recommended)
  • Auth: bcrypt + JWT (via jose, Edge-compatible) + email OTP verification
  • Email: SendGrid, with automatic console-log fallback when no API key is set
  • Images: Cloudinary, with automatic console-log fallback when not configured
  • Validation: Zod on every API input
  • Multi-tenancy: subdomain-based by default (store.returnpilot.com), switchable to path-based or custom-domain via src/lib/tenant.ts + src/middleware.ts only

Local setup

  1. npm install
  2. Copy .env.example to .env and fill in DATABASE_URL/DIRECT_URL (Neon free tier: https://neon.tech)
  3. npx prisma migrate dev --name init
  4. npm run seed -- creates a demo store, owner login, and sample claims
  5. npm run dev
  6. Visit http://demo-store.localhost:3000 for the seeded store, or http://localhost:3000 for the marketing site

IMPORTANT: dashboards and store portals only exist on a store's own subdomain (e.g. http://demo-store.localhost:3000/dashboard), never on plain http://localhost:3000/dashboard -- that's the root/marketing domain, which has no dashboard route at all and will 404. Most browsers (Chrome, Firefox, Safari, Edge) resolve any *.localhost subdomain to your own machine automatically, with no /etc/hosts changes needed. Logging in or signing up from the root domain now correctly sends you to your store's subdomain automatically -- you don't need to type the subdomain URL in by hand.

Demo logins (from seed):

Switching domain strategy later

Only two files matter: src/lib/tenant.ts (resolution logic) and src/middleware.ts (rewrite rules). Set TENANCY_MODE=path in .env to switch from subdomains to /store/ paths -- no other file needs to change. Custom domains (a store's own domain) work in either mode automatically once Store.customDomain is set.

SendGrid / Cloudinary fallback behavior

Both integrations are optional in development. If their env vars are empty, OTP codes and email content print to the server console instead of failing, and uploaded images are logged instead of stored. This lets the whole app function end-to-end before you activate paid/free-tier accounts for either service.

Deployment (Vercel + Neon)

  1. Push this repo to GitHub
  2. Import into Vercel
  3. Vercel dashboard -> Storage -> Connect Database -> Neon (auto-injects DATABASE_URL/DIRECT_URL)
  4. Add remaining env vars from .env.example in Vercel project settings
  5. Add a wildcard DNS record *.yourdomain.com pointing at Vercel for subdomain tenancy to resolve
  6. Set NEXT_PUBLIC_APP_URL to your real live URL once you have one -- this feeds every page's SEO title/description and the auto-generated robots.txt/sitemap.xml, so you only need to change it in one place (your env vars), not in any code file
  7. Deploy (vercel.json in this repo also schedules the free daily email digest cron job -- no extra setup needed)

SEO

Every page's title/description comes from Next.js's built-in Metadata API (not react-helmet -- Helmet only updates the page after it loads in the browser, which search engines and link-preview crawlers miss; the Metadata API renders it into the actual server-sent HTML, which is what real SEO needs). robots.txt and sitemap.xml are both generated automatically from src/app/robots.ts and src/app/sitemap.ts. Both currently point at a placeholder URL (https://returnpilot.com) -- once you have your real live URL, set NEXT_PUBLIC_APP_URL in your env vars and everything (metadata, robots.txt, sitemap.xml) updates from that one setting. Private areas (dashboard, platform-admin, claim tracking links) are marked noindex so they never show up in search results.

Docker & Kubernetes

Dockerfile, .dockerignore, compose.yaml, and the k8s/ folder are all included for testing this outside of Vercel. Every file is commented in plain language, and docs/docker-and-kubernetes.md walks through each step as its own separate command. Quick start locally:

docker compose up

then in a second terminal:

docker compose exec web npx prisma migrate deploy

Full walkthrough (including the Kubernetes steps) is in docs/docker-and-kubernetes.md.

Billing (Stripe, test mode)

Stripe is free to integrate (no monthly fee -- only a cut of real transactions, and test mode is entirely free). Leave STRIPE_* vars empty in .env and the billing page still renders normally; clicking "Upgrade" shows a clear "billing not configured yet" message instead of crashing. To enable it:

  1. Create a free Stripe account, switch to test mode
  2. Create a recurring $19/mo price for the Growth plan, copy its price ID into STRIPE_GROWTH_PRICE_ID
  3. Copy your test secret key into STRIPE_SECRET_KEY
  4. Run stripe listen --forward-to localhost:3000/api/webhooks/stripe locally (Stripe CLI, free) and copy the printed webhook signing secret into STRIPE_WEBHOOK_SECRET
  5. Use Stripe's test card 4242 4242 4242 4242 to test a full checkout

Staff invites

Store owners can invite team members from Dashboard > Settings > Team. Invite emails (or console-logged fallback content) contain a link to /staff/accept-invite?token=... where the invitee sets a name and password.

Rate limiting

Login, signup, and OTP verification are all rate-limited using a Postgres-backed sliding window (src/lib/rateLimit.ts) -- no extra paid service required.

Forgot / reset password

/forgot-password and /reset-password (both on the marketing root domain) let any user request a reset link. The API always returns the same response whether or not the account exists, to avoid leaking which emails are registered. Reset tokens expire after 1 hour and are single-use.

Platform admin panel

A separate super-admin panel lives at /platform-admin (root domain, not a store subdomain) for you as the platform owner to see and manage every store. Log in at /platform-admin/login with a SUPER_ADMIN account (the seed script creates one: admin@returnpilot.test / Demo1234!). From there you can view claim/order/user counts per store and suspend or reactivate any store. This route isn't linked from the public navbar by design.

CSV export

Sellers can export claims and orders as CSV from the dashboard ("Export CSV" buttons on those pages), or directly via GET /api/claims/export and GET /api/orders/export (auth required).

Daily email digest (Vercel Cron)

vercel.json schedules a free daily cron job that hits /api/cron/daily-digest, which emails each store owner a summary of new and pending claims. Set CRON_SECRET in your Vercel project env vars once deployed so the endpoint only responds to genuine Vercel cron requests; it's optional locally.

CSRF protection + security headers

State-changing dashboard/admin requests (claim status updates, policy changes, staff invites, store suspension) are protected by a double-submit CSRF cookie (GET /api/csrf issues the token; the client's fetchWithCsrf() helper in src/lib/fetchWithCsrf.ts attaches it automatically). next.config.js also sets baseline security headers (X-Frame-Options, X-Content-Type-Options, Referrer-Policy, Permissions-Policy).

Health check

GET /api/health checks the database connection and returns 200/503 accordingly -- point any free uptime monitor (UptimeRobot, Better Uptime, etc.) at it.

Advanced features (beyond a typical small-store returns tool)

Store credit & exchange incentives -- sellers can issue bonus store credit (configurable %, default 10%) instead of a cash refund from the claims table. This is the single biggest lever real returns platforms use to retain revenue instead of losing it. Credits are tracked at Dashboard > Store Credits.

Analytics dashboard (Dashboard > Analytics) -- return rate, revenue at risk/refunded, a 14-day claims trend line, claims by status/type, and top return reasons, all computed live from your data with recharts.

Serial-returner risk scoring -- the Customers page flags each customer low/medium/high risk based on their claim-to-order ratio, computed in src/lib/riskScore.ts (no external service, fully explainable).

Auto-approval rules engine (Dashboard > Settings > Auto-Approval Rules) -- define rules by claim type, max item price, and/or reason keywords; matching claims are approved instantly on submission instead of waiting for manual review.

Bulk actions + internal staff notes -- select multiple claims in the dashboard table to approve/reject/ mark-under-review at once, and leave staff-only internal notes on any claim (never shown to the customer).

Public API + outbound webhooks (Dashboard > Settings > Developer) -- generate API keys (SHA-256 hashed, shown once) to call GET /api/public/v1/claims and /api/public/v1/claims/:id from your own tools, and register webhook URLs to receive HMAC-signed POSTs on claim.created/updated/approved/rejected. Most small-store tools gate this behind enterprise tiers -- it's standard here.

More features added post-v1

  • Order CSV bulk import (Dashboard > Orders > Import Orders CSV) -- upload orders exported from your e-commerce platform instead of entering them manually. Expected columns: order_number, customer_name, customer_email, item_name, item_price, purchase_date.
  • Search + pagination on the Orders and Claims dashboard tables.
  • Claim detail page (click any order number in the claims table) -- full order/customer info, photos, timeline, and internal notes together in one view, instead of only a table row.
  • Structured return reason categories -- customers pick from a dropdown (wrong size, defective, damaged in shipping, etc.) instead of only free text, which also improves the analytics dashboard's accuracy.
  • Store branding (Dashboard > Settings > Branding) -- upload a logo and set a brand color that appears on your customer-facing return portal pages.
  • Two-factor authentication (Dashboard > Settings > Security) -- TOTP-based (Google Authenticator, Authy, etc.), free and standard, no SMS service required. Once enabled, login requires both a password and a 6-digit code.
  • Self-service password change (Dashboard > Settings > Security).
  • Terms of Service acceptance required at signup (placeholder text at /terms -- replace before launch).
  • Audit log (Dashboard > Settings > Audit Log, owner-only) -- records staff actions like policy changes and invites, for accountability.
  • Inbound order API -- POST /api/public/v1/orders lets your own e-commerce platform push new orders in automatically (same API key auth as the rest of the public API), as an alternative to CSV import.

Bug fixes worth knowing about

These were found and fixed after initial testing -- noting them here since they affect how login/logout and multi-tenant routing behave:

  • Cross-subdomain session cookies: the session cookie now has a Domain attribute (src/lib/tenant.ts, getSessionCookieDomain()) so it's shared across every store's subdomain, not just the exact host that set it. Without this, logging in on the root domain produced a cookie the store's subdomain never received.
  • Login/signup redirects: after logging in or completing signup verification, the app now redirects you to a full URL on your store's own subdomain (src/lib/tenant.ts, buildTenantUrl()) instead of a relative "/dashboard" path, which only exists on subdomains and 404s on the root domain.
  • Dashboard pages now actually check you're logged in: previously, dashboard pages fetched and displayed data directly with no session check at all -- only the action buttons (approve/reject etc.) verified auth via their API calls. src/app/tenant/dashboard/layout.tsx now redirects anyone who isn't a logged-in owner/ staff member of that specific store (or a SUPER_ADMIN) back to the login page.
  • /api/ routes no longer get tenant-rewritten*: they resolve their store from the request's Host header directly, so rewriting them to /_store/api/... (as pages need) was breaking every API call made from a subdomain.
  • The tenant pages folder was named starting with an underscore (_store), which Next.js treats as a private, non-routable folder by design -- meaning /dashboard, /portal/submit, and every other tenant page could never actually be reached no matter what the middleware rewrote the URL to. Renamed to src/app/tenant/ (no leading underscore) and updated the one line in src/middleware.ts that references it. This was the real cause of the persistent "page not found" on store subdomains.

Project structure notes

  • src/app/(marketing)/ -- public marketing site (landing, pricing, login, signup), rendered on the root domain
  • src/app/tenant/ -- everything resolved for a tenant (subdomain or custom domain) via middleware rewrite
    • src/app/tenant/portal/ -- customer-facing return submission + tracking
    • src/app/tenant/dashboard/ -- seller admin dashboard (sidebar + navbar synced via SidebarContext)
  • src/app/api/ -- all backend routes, every one tenant-scoped server-side via src/lib/getTenant.ts
  • src/lib/ -- prisma client, tenant resolution, auth (jose JWT), mailer (SendGrid+fallback), cloudinary (+fallback), validation (Zod)
  • prisma/seed.ts -- mandatory demo data seeding (store, users, orders, claims, notifications)

About

Multi-tenant e-commerce returns and warranty management SaaS with automated claim processing, return analytics, store credits, customer risk scoring, and seller workflows.

Topics

Resources

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages