尚未偵測到更新。GitHub 發布版本後會顯示在這裡。
個人 GitHub 帳號下公開儲存庫的擁有者可以登入並驗證所有權,自行管理介紹,無需管理員核准。
登入並產生驗證檔案,在 30 分鐘內將其提交至此儲存庫的預設分支。其他分支或分叉儲存庫中的檔案無效。然後在下方檢查檔案,即可無需管理員核准直接管理專案。請保留檔案以維持管理權限。
如需登入,將前往登入頁面。登入後請回到此處,再次點擊所選的驗證按鈕。
在專案資料夾中執行 /nomorevibe verify,即可驗證所有權並自行管理介紹。
一分鐘完成驗證製作者的介紹僅有上面的一句話。 更多詳情請閱讀儲存庫 README。 儲存庫 README ↗
正在載入完整 README。
檢視完整 README ↗Multi-tenant enrollment management platform for Myanmar language schools.
SaaS platform supporting MMK currency, bilingual Myanmar + English interface, subdomain-based multi-tenancy, and role-based access control.
KuuNyi enables Japanese language schools in Myanmar to manage student enrollments online. Each school gets a dedicated subdomain (e.g. nihon-moment.kuunyi.com) with:
| Layer | Technology |
|---|---|
| Framework | Next.js 14 (App Router) |
| Language | TypeScript |
| Styling | Tailwind CSS |
| Fonts | Noto Sans + Noto Sans Myanmar (Google Fonts) |
| Database | Supabase (PostgreSQL + Storage + Auth) |
| Auth | Supabase Auth via @supabase/ssr |
| Charts | Recharts |
| Excel Export | SheetJS (xlsx) |
| Deployment | Vercel (wildcard subdomain routing) |
| Linting | ESLint + Prettier |
Create .env.local from the example:
cp .env.local.example .env.local
| Variable | Required | Description |
|---|---|---|
NEXT_PUBLIC_SUPABASE_URL | Yes | Supabase project URL (e.g. https://xxx.supabase.co) |
NEXT_PUBLIC_SUPABASE_ANON_KEY | Yes | Supabase anonymous/public key |
SUPABASE_SERVICE_ROLE_KEY | Yes | Supabase service role key (server-side only, bypasses RLS) |
MESSENGER_APP_ID | No | Meta Developer App ID (for Messenger Bot) |
MESSENGER_APP_SECRET | No | Meta Developer App Secret |
MESSENGER_ENCRYPTION_KEY | No | 64-char hex for AES-256-GCM page token encryption |
MESSENGER_VERIFY_TOKEN | No | Verify token for central webhook (e.g. kuunyi-webhook-2026) |
git clone <repo-url>
cd KuuNyi
npm install
cp .env.local.example .env.local
# Fill in your Supabase project URL, anon key, and service role key
npx supabase login
npx supabase link --project-ref <your-project-ref>
npm run db:migrate
npm run dev
Open http://localhost:3005.
?tenant= query param:http://localhost:3005/enroll/april-2026?tenant=nihon-moment
http://localhost:3005/admin/dashboard?tenant=nihon-moment
nihon-moment.kuunyi.com → slug nihon-moment)?tenant=nihon-moment query param for local developmentx-tenant-slug header → resolveTenantId() looks up tenant_id from tenants tablerequireAuth() resolves tenant from user profile (via Supabase session)resolveTenantId() resolves tenant from middleware header/register, /api/saas/*, /superadmin, /onboarding bypass tenant detectionSee SUBDOMAIN_SETUP.md for detailed Vercel wildcard domain configuration.
| Role | Scope | Permissions |
|---|---|---|
superadmin | Platform-wide | View all schools, suspend/activate tenants, access super admin panel. Cannot access individual school admin dashboards. |
owner | Single tenant | Full access within their school — manage staff, bank accounts, school profile, export data, view analytics. Created on registration. |
staff | Single tenant | View dashboard, intakes, students, payments, announcements, analytics. Cannot access Settings, Export, or bank account management. Added via invite. |
Enforcement layers:
requireOwner() in API routes returns 403 for staffbank_accounts, tenants, and other users/superadmin routes protected by layout-level role checksrc/
├── app/
│ ├── page.tsx # SaaS marketing landing page
│ ├── login/ # Admin login page
│ ├── register/ # School registration form (subdomain check)
│ ├── onboarding/ # 4-step post-registration wizard
│ ├── superadmin/ # Super admin panel (superadmin-only)
│ │ ├── layout.tsx # Auth guard — redirects non-superadmin
│ │ ├── page.tsx # Schools list, stats, suspend/activate
│ │ └── schools/[id]/ # School detail page
│ ├── admin/ # Admin dashboard (protected, requires auth)
│ │ ├── layout.tsx # Auth guard + sidebar shell + RoleProvider
│ │ ├── dashboard/ # Stats overview, recent enrollments
│ │ ├── intakes/ # Intake list + create modal
│ │ │ └── [id]/ # Intake detail: classes, enrollment link
│ │ ├── students/ # Student records, filters, Excel export
│ │ ├── analytics/ # Recharts dashboard (trend, distribution, fill)
│ │ ├── payments/ # Payment verification queue
│ │ ├── announcements/ # Announcement composer + history
│ │ └── settings/ # Bank accounts CRUD + school profile
│ │ ├── staff/ # Staff management (owner-only)
│ │ └── billing/ # Billing placeholder (Free Beta)
│ ├── (public)/
│ │ ├── layout.tsx # Public layout — header, footer, no sidebar
│ │ ├── status/ # Enrollment status checker (search by ref)
│ │ └── enroll/
│ │ ├── [slug]/ # Intake landing page — class cards, bilingual
│ │ ├── form/ # Two-step enrollment form
│ │ └── payment/[ref]/ # Payment instructions + proof upload
│ ├── api/
│ │ ├── saas/
│ │ │ ├── check-subdomain/ # GET — subdomain availability check
│ │ │ └── register/ # POST — create tenant + auth user + profile
│ │ ├── superadmin/schools/ # GET list / GET [id] detail / PATCH suspend
│ │ ├── intakes/ # GET/POST intakes, GET/POST classes
│ │ ├── classes/[id]/ # PATCH class
│ │ ├── admin/
│ │ │ ├── stats/ # GET dashboard stats
│ │ │ ├── analytics/ # GET analytics (trend, distribution, fill)
│ │ │ ├── students/ # GET paginated students + GET [id] detail
│ │ │ ├── payments/pending/ # GET pending payment queue
│ │ │ ├── payments/[id]/verify/ # PATCH approve/reject
│ │ │ ├── bank-accounts/ # GET/POST bank accounts
│ │ │ ├── bank-accounts/[id]/ # PATCH/DELETE bank account
│ │ │ ├── announcements/ # GET/POST announcements
│ │ │ └── staff/ # GET list / POST invite (owner-only)
│ │ │ └── accept/ # GET accept invite token
│ │ └── public/
│ │ ├── enroll/ # POST enrollment submission
│ │ ├── enroll/[slug]/ # GET intake + classes for public landing
│ │ ├── bank-accounts/ # GET active bank accounts (tenant-scoped)
│ │ ├── payments/upload/ # POST payment proof upload
│ │ └── status/ # GET enrollment status by ref
│ └── layout.tsx
├── components/
│ ├── ui/ # StatusBadge, StatsCard, ConfirmModal,
│ │ # Toast, LoadingSpinner, EmptyState
│ └── admin/ # Sidebar, LogoutButton, RoleContext
├── lib/
│ ├── supabase/
│ │ ├── client.ts # Browser client
│ │ ├── server.ts # Server client (cookies)
│ │ └── admin.ts # Service-role client (bypasses RLS)
│ ├── api.ts # requireAuth(), requireOwner(), resolveTenantId()
│ ├── tenant.ts # extractSubdomainFromHost() helper
│ ├── utils.ts # formatMMK, formatMyanmarPhone, etc.
│ └── messenger/
│ ├── send.ts # Meta Graph API send helpers
│ ├── processor.ts # Message routing + keyword matching
│ ├── responses.ts # 6 bot response handlers
│ └── crypto.ts # AES-256-GCM encrypt/decrypt for page tokens
└── types/
└── database.ts # TypeScript types for all tables + enums
supabase/
├── config.toml # Supabase CLI config
└── migrations/
├── 000_combined_schema.sql # Combined initial schema
├── 001–013_*.sql # Individual table/policy migrations
└── 014_staff_roles.sql # Staff roles, invites, RLS updates
| Route | Description |
|---|---|
/ | Marketing landing page — hero, feature cards, CTA |
/register | School registration — name EN/MM, subdomain check, admin email/password |
/onboarding | 4-step wizard — profile, bank accounts, first intake, enrollment link |
| Route | Description |
|---|---|
/superadmin | Stats row, schools table, suspend/activate toggle |
/superadmin/schools/[id] | School detail — owner, intakes, revenue, suspend modal |
| Route | Description |
|---|---|
/admin/dashboard | Stats cards, seats overview, recent enrollments, 60s auto-refresh |
/admin/intakes | List intakes, create new intake modal |
/admin/intakes/[id] | Classes table, Add All 5 Classes, Copy Enrollment Link |
/admin/students | Paginated table, filters, Student Detail Modal, Excel export |
/admin/analytics | Enrollment trend, class distribution, seat fill, date range filter |
/admin/payments | Pending payment card grid, review modal, approve/reject |
/admin/announcements | Composer (intake + class selector + message), sent history |
/admin/settings | Bank accounts CRUD, school profile, Messenger Bot, change password |
/admin/settings/staff | Staff management — list, invite via email (owner-only) |
/admin/settings/billing | Free Beta placeholder |
| Route | Description |
|---|---|
/enroll/[slug] | Intake landing — class cards, level badges, fees, seats remaining |
/enroll/form | Two-step bilingual enrollment form |
/enroll/payment/[ref] | Payment instructions + proof upload |
/status | Enrollment status checker — search by ref |
| Method | Path | Description |
|---|---|---|
GET | /api/saas/check-subdomain?slug=xxx | Check subdomain availability |
POST | /api/saas/register | Create tenant + auth user + owner profile |
| Method | Path | Description |
|---|---|---|
GET | /api/superadmin/schools | List all schools with stats |
GET | /api/superadmin/schools/[id] | School detail with intakes, revenue |
PATCH | /api/superadmin/schools/[id] | Suspend or activate a school |
| Method | Path | Description |
|---|---|---|
GET | /api/public/enroll/[slug] | Classes for an intake |
POST | /api/public/enroll | Submit enrollment (atomic, seat-safe) |
POST | /api/public/payments/upload | Upload payment proof (max 5 MB) |
GET | /api/public/bank-accounts | Active bank accounts |
GET | /api/public/status?ref=NM-YYYY-NNNNN | Check enrollment + payment status |
| Method | Path | Description |
|---|---|---|
GET | /api/messenger/webhook | Central webhook verification (hub.challenge) |
POST | /api/messenger/webhook | Receive messages, route by page_id to tenant |
GET | /api/messenger/webhook/[slug] | Per-school webhook verification (legacy fallback) |
POST | /api/messenger/webhook/[slug] | Per-school message handler (legacy fallback) |
GET | /api/messenger/connect/[slug] | Redirect to Meta OAuth for page connection |
GET | /api/messenger/callback | OAuth callback — exchange code, save encrypted token |
GET | /api/messenger/settings | Read messenger config (auth required) |
PATCH | /api/messenger/settings | Update messenger config (auth required) |
| Method | Path | Description |
|---|---|---|
GET | /api/admin/stats | Dashboard counts and seat summary |
GET | /api/admin/analytics | Analytics data (trend, distribution, fill, conversion) |
GET | /api/admin/students | Paginated students with filters |
GET | /api/admin/students/[id] | Full student detail + signed proof URL |
GET | /api/admin/payments/pending | Payment verification queue |
PATCH | /api/admin/payments/[id]/verify | Approve or reject a payment |
GET | /api/admin/bank-accounts | List bank accounts |
POST | /api/admin/bank-accounts | Add bank account |
PATCH | /api/admin/bank-accounts/[id] | Toggle active / update |
DELETE | /api/admin/bank-accounts/[id] | Delete bank account |
GET | /api/admin/announcements | List announcements |
POST | /api/admin/announcements | Save announcement |
GET | /api/admin/staff | List staff members (owner-only) |
POST | /api/admin/staff | Create staff invite (owner-only) |
GET | /api/admin/staff/accept?token=xxx | Accept invite, create account |
GET | /api/intakes | List intakes |
POST | /api/intakes | Create intake |
GET | /api/intakes/[id]/classes |
9 tables with full Row Level Security (multi-tenant, scoped by tenant_id):
| Table | Description |
|---|---|
tenants | School organisations (name, subdomain, plan, messenger config) |
users | Admin staff (superadmin / owner / staff roles) |
intakes | Enrollment cohorts (e.g. April 2026 Intake) |
classes | JLPT levels N5–N1 per intake, with seat tracking |
enrollments | Student records with auto-generated NM-YYYY-NNNNN refs |
payments | Payment proof submissions with verification status |
bank_accounts | School bank accounts shown to students |
announcements | Admin-composed announcements |
staff_invites | Invite tokens for staff onboarding (7-day expiry) |
Key patterns:
submit_enrollment() — PostgreSQL RPC with SELECT FOR UPDATE to prevent oversellingtrg_payments_sync_enrollment — advances enrollment status when payment is insertedpayment-proofs — private, tenant-scoped, signed URLs for admin accessgetUser() (not getSession()) for server-side auth validationget_my_role() — SECURITY DEFINER function for RLS policy role checks*.kuunyi.com (or *.edu-enroll-xi.vercel.app as fallback)See SUBDOMAIN_SETUP.md for detailed wildcard subdomain configuration.
npx supabase link --project-ref <ref>npm run db:migratepayment-proofs storage bucket (private)Run the tenant isolation test to verify cross-tenant data access is blocked:
export SUPABASE_URL=https://xxx.supabase.co
export SUPABASE_SERVICE_ROLE_KEY=xxx
bash security-audit.sh
Each tenant can connect their Facebook Page to enable an auto-reply bot for student inquiries.
https://kuunyi.com/api/messenger/callback| Variable | Description |
|---|---|
MESSENGER_APP_ID | Meta App ID from the Developer Console |
MESSENGER_APP_SECRET | Meta App Secret |
MESSENGER_ENCRYPTION_KEY | 64-char hex string for AES-256-GCM encryption of page tokens |
Generate the encryption key:
node -e "console.log(require('crypto').randomBytes(32).toString('hex'))"
pages_messaging + pages_read_engagement + pages_show_list permissionstenants table| Button | Payload | Response |
|---|---|---|
| Open Intakes | OPEN_INTAKES | Lists open intakes with class levels and availability |
| Fees | FEES | Shows course fees per JLPT level in Myanmar numerals |
| How to Enroll | HOW_TO_ENROLL | Sends tenant-specific enrollment URL |
| Schedule | SCHEDULE | Shows enrollment open/close dates (EN + Myanmar) |
| Payment | PAYMENT | Lists active bank accounts + payment proof upload link |
| Check Status | CHECK_STATUS | Prompts for ref number, returns enrollment + payment status |
The bot also supports keyword matching for free-text messages (e.g. "fee", "enroll", "ငွေ") and reference number detection (e.g. NM-2026-00042).
One central webhook handles all schools:
https://kuunyi.com/api/messenger/webhook
Configure this in the Meta Developer Console under Webhooks → Callback URL, with MESSENGER_VERIFY_TOKEN as the verify token. Messages are routed to the correct school by matching entry[].id (page ID) against tenants.messenger_page_id.
| Command | Description |
|---|---|
npm run dev | Start dev server (port 3005) |
npm run build | Build for production |
npm run lint | Run ESLint |
npm run format | Format code with Prettier |
npm run db:migrate | Push pending migrations to Supabase |
npm run db:status | List migration status (local vs remote) |
npm run db:diff | Diff local schema vs remote (requires Docker) |
npm run db:reset | Reset local database (WARNING: deletes all data) |
The interface supports both Myanmar and English. Prices are displayed in MMK (Myanmar Kyat).
| List classes for intake |
POST | /api/intakes/[id]/classes | Create class |
PATCH | /api/classes/[id] | Update class |