Before you start
Each installation needs its own Supabase project. Never reuse a database from another product or share your service-role key in the browser.
1. Create your Supabase project
- Create a project at supabase.com. Choose the region closest to your students. Save the database password.
- Open Project Settings → API and copy the Project URL, the publishable key, and the secret key (or legacy service_role key).
- Configure Auth URLs under Authentication → URL Configuration: set the Site URL to your domain and add
https://your-domain.com/auth/callback. - Email delivery uses Supabase Auth defaults. For production volume, connect your own SMTP provider under Authentication → SMTP.
2. Run the database schema
The zip includes a single consolidated schema.sql. Open the Supabase SQL Editor, paste the whole file and run it once. It is written to be safe to re-run.
-- Option A: Supabase Dashboard → SQL Editor → paste schema.sql → Run
-- Option B: from the project folder with the Supabase CLI
supabase link --project-ref YOUR_PROJECT_REF
supabase db pushAfter the schema runs you should see the core tables: profiles, courses, payments, certificates and the license cache table used by the install wizard.
3. Deploy to Vercel
Unzip the release, push it to a private repository you own, then import the repository into Vercel. The build command is pnpm build and the framework preset is Next.js.
unzip nahiyan-lms-v1.0.0.zip
cd nahiyan-lms
pnpm install
git init && git add . && git commit -m "Initial Nahiyan LMS"
gh repo create my-academy --private --source=. --push
# or deploy directly with the Vercel CLI
vercel link
vercel deploy --prod4. Environment variables
Add these to Vercel → Project → Settings → Environment Variables (Production and Preview). Locally, copy them into .env.local.
NEXT_PUBLIC_SUPABASE_URL=https://YOUR-PROJECT.supabase.co
NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY=sb_publishable_xxx
SUPABASE_SERVICE_ROLE_KEY=sb_secret_xxx
NEXT_PUBLIC_SITE_URL=https://your-domain.com
NEXT_PUBLIC_LICENSE_REQUIRED=true
NEXT_PUBLIC_LICENSE_SERVER_URL=https://store.nahiyanacademy.com
CRON_SECRET=generate-a-long-random-value| Variable | Purpose |
|---|---|
| NEXT_PUBLIC_SUPABASE_URL | Your Supabase project URL |
| NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY | Browser-safe publishable / anon key |
| SUPABASE_SERVICE_ROLE_KEY | Server-only key for payments and license checks |
| NEXT_PUBLIC_SITE_URL | Your public https origin |
| NEXT_PUBLIC_LICENSE_REQUIRED | Set to true for production installs |
| NEXT_PUBLIC_LICENSE_SERVER_URL | License server, normally the store URL |
| CRON_SECRET | Protects scheduled notification routes (optional) |
5. Activate with /install
The first time you open the deployment, the license gate redirects every route to /install. The wizard checks database connectivity, activates your key and binds the current domain to a slot.
Database check
The wizard verifies your Supabase URL and secret key can read the schema.
License activation
Paste the key from your dashboard. The store verifies it and returns a signed 24-hour status.
Domain binding
The current hostname is bound automatically if a slot is free. Re-validates every 24 hours.
Domains are normalised: protocol, www. and ports are removed. www.example.com and example.com count as the same domain. Vercel preview domains bind a slot too — use your production domain.
6. Create your academy admin
Register a normal account on your deployment, then promote it in the Supabase SQL Editor. The LMS uses uppercase roles.
update public.profiles
set role = 'ADMIN'
where id = (select id from auth.users where email = 'you@example.com');Then sign in and open /admin. Configure site branding, payment settings, instructors and your first course.
7. Manage domains
Open your buyer dashboard to see every bound domain and its slot usage. To move a license:
- Unbind the old domain from the dashboard. The slot frees immediately.
- Deploy the same code to the new domain.
- Open
/installand run activation again. The new hostname binds automatically.
Regular includes one live domain; Extended includes five. If all slots are used, activation returns domain_limit_reached until you unbind one.
8. Updates & releases
New versions appear in your dashboard changelog. Downloads use a signed URL that expires after ten minutes, so links cannot be shared indefinitely. To update:
# keep your env and database, replace the code
unzip nahiyan-lms-vX.Y.Z.zip -d nahiyan-lms-next
rsync -a --exclude .env.local --exclude node_modules \
nahiyan-lms-next/ ./nahiyan-lms/
pnpm install && pnpm build
vercel deploy --prodRun any new SQL from the release notes before deploying. Your license stays bound to the same domain.
Troubleshooting
The app keeps redirecting to /install+
The cached license is missing, expired, or the hostname does not match. Open /install and re-run activation. Confirm NEXT_PUBLIC_LICENSE_REQUIRED is true and the server URL is reachable.
domain_limit_reached+
Every slot is bound. Unbind an unused domain in the store dashboard, then retry activation.
Database check fails+
Verify NEXT_PUBLIC_SUPABASE_URL and SUPABASE_SERVICE_ROLE_KEY in Vercel, redeploy, and confirm schema.sql ran completely.
Auth emails do not arrive+
Supabase's default mailer is rate-limited. Configure custom SMTP in Authentication → SMTP for production.
bKash checkout unavailable+
The store's bKash merchant credentials are not configured yet. Use the manual payment request and the admin can approve it.
Build fails on Vercel+
Use Node 20+, run pnpm install locally to confirm the lockfile, and make sure all required env vars exist in the Vercel project.
License API reference
The install wizard calls this endpoint. You can also use it for custom deployments.
curl -X POST https://store.nahiyanacademy.com/api/license/verify \
-H "Content-Type: application/json" \
-d '{"licenseKey":"STH-LMS-REG-XXXX-XXXX","domain":"academy.example.com"}'
# success
{
"valid": true,
"tier": "REGULAR",
"licenseKey": "STH-LMS-REG-XXXX-XXXX",
"domain": "academy.example.com",
"maxDomains": 1,
"expiresAt": "2026-11-16T10:00:00.000Z",
"signature": "<hmac-sha256>"
}
# failure
{ "valid": false, "reason": "domain_limit_reached" }The signature is an HMAC-SHA256 of licenseKey|tier|domain|expiresAt using the store signing secret. Cache a successful response for up to 24 hours, then revalidate.
Still stuck?
Open a ticket from your dashboard and include your domain, license key prefix and the exact error. Extended licenses get priority.
Open a support ticket