A unified education, digital services, and enterprise solutions platform for business growth
WeTrainEducation & Tech is a full-scale digital services platform combining professional training, enterprise software solutions, marketing services, customer management, payments, order processing, certifications, project inquiries, and admin operations within one connected ecosystem.

One Supabase identity, four business systems, one deployable Next.js application. A public marketing site plus three internal products (Education, CRM, HRM) and an internal Store, built so that each business area can change without putting the others at risk.
| Area | Details |
|---|---|
| Role | Architecture, full-stack engineering, database design, DevOps |
| Stack | Next.js 16 (App Router, proxy.ts), React 19, TypeScript, Supabase (Auth, Postgres, RLS, Storage) |
| UI | Tailwind CSS 4, shadcn/ui on Radix, Recharts, TanStack Table, next-intl (EN/BN) |
| Integrations | SMS Pay BD (payments + webhooks), SMTP (Nodemailer), Vercel Cron |
| Delivery | Vercel, GitHub Actions CI, Supabase CLI migrations |
| Status | In production use; actively evolving |
WeTrainEducation & Tech sells training courses, software and marketing services, and runs an internal organisation around that. In practice that means four very different groups of people use the same company's systems:
| Persona | What they need | Shape of their data |
|---|---|---|
| Visitors & students | Discover services, request proposals, pay, track subscriptions | Unbounded growth, mostly self-service |
| Sales team | Leads, a deal pipeline, contracts, recurring billing | Hundreds of records, ownership-based |
| Staff & interns | Weekly reporting, scoring, recruitment, attendance | Tens of people, hierarchy-based |
| Office operations | A tiny internal shop with stock, invoices and per-employee ledgers | Money-like data that must stay consistent |
Off-the-shelf tools covered each need badly and none of them shared an identity. The brief was to replace the patchwork with one coherent platform, without building four separate apps that would each need their own login, deploy and on-call.
Asia/Dhaka time for all scheduled work, Bangla as a first-class language on the public site.A modular monolith: one Next.js application with hard internal boundaries, one Supabase project as the single source of identity and data, and a layered authorization model that fails closed.
| Metric (measured on this repo) | Value |
|---|---|
| TypeScript / TSX source files | ~590 (~100k lines) |
| Pages / route handlers / layouts | 119 / 83 / 13 |
Server-action modules ("use server") | 34 |
Route-level loading skeletons (loading.tsx) | 26 |
| SQL migrations | 104, all applying cleanly from an empty database |
| Tables / RLS policy statements | 90+ / ~400 |
| Scheduled jobs (Vercel Cron) | 6 |
Automated tests (node:test) | 88 across 6 suites, all passing |
Every route belongs to exactly one module. Shared code is deliberately small, and a module never imports another module's business logic.
| Module | Route prefix | Owns (tables) | Roles |
|---|---|---|---|
| Landing | /(landing) | services, certifications, featured_projects, client_stories, proposals (insert only) | anonymous |
| Education | /dashboard/customer, /dashboard/admin | profiles, orders, payments (customer-facing views of CRM billing and documents) | customer, admin |
| CRM | /dashboard/crm | crm_users, crm_leads, deals, proposals, documents, billing_schedules, invoices, payment_links, crm_contact_logs | ADMIN, MARKETER |
| HRM | /dashboard/hrm | hrm_users, hrm_weeks, hrm_task_*, hrm_intern_* | SUPER_ADMIN, ADMIN, EMPLOYEE |
| Store | /dashboard/store | store_users, store_products, store_stock_movements, store_invoices, store_account_entries | USER, ADMIN + permissions |
The arrows only ever point down into the shared kernel. CRM links to a customer by profile_id, never by importing Education code. This is what lets an engineer, or an AI agent, work on HRM payroll-adjacent logic with confidence that Education checkout cannot break.
Each decision below is recorded as a short ADR: context, decision, alternatives and the cost we accepted. The longer-form originals live in docs/.
_actions, _components, _lib, _constants) and written rules, not by network hops.auth.users), separate domain profiles per moduleThis is the load-bearing decision of the whole system.
auth.users is the only identity. Each module owns a table whose primary key is the auth user id: profiles, crm_users, hrm_users, store_users. Having a row = having access to that module.users table with ~20 mostly-null columns and a shared RLS policy; (B) separate auth per system (no SSO, email collisions, three bills).MARKETER never collides with EMPLOYEE), each table gets its own indexes and RLS, and a bug in one module's schema cannot corrupt another's.Identity: every module table shares the primary key of auth.users.
Education and CRM: customer money flow and the sales pipeline.
HRM and Store: weekly reporting, recruitment, and the ledger-backed shop.
A user with several memberships needs a deterministic home. The rule is Education > CRM > HRM > Store, with an explicit module switcher (see the product tour) for everything else.
The same priority table exists in two places on purpose: proxy.ts for fast redirects and app/dashboard/page.tsx for the server-rendered entry. The written rule in AGENTS.md and docs/CRITICAL_SYSTEMS.md keeps them aligned; a shared, tested routing function is on the roadmap.
getCurrentUserWithRoles()UserWithRoles (education role, CRM role, HRM role, Store role + granular Store permissions, derived capability flags). Guards (requireCrmAdmin(), requireHrmSuperAdmin(), requireStorePermission(...)) are thin wrappers over it.activeHrmRole).// app/utils/auth/require.ts (excerpt): every guard is the same two steps
export async function requireCrmAdmin(): Promise<UserWithRoles> {
const roles = await requireAuth(); // 1. signed in + has any module
if (!isCrmAdmin(roles)) redirect("/unauthorized"); // 2. has this specific capability
return roles;
}
Next.js 16 renamed middleware to proxy and its own documentation steers authors away from doing heavy authorization there. We follow that guidance:
proxy.ts does only what must happen on every request: refresh the Supabase cookie, redirect anonymous users, send logged-in users to the right app home.requireXAccess()), and finally in Postgres RLS./login, never waved through. Public routes are never blocked by an infrastructure blip.See Section 6 for the full picture.
app/**/_actions/. Route Handlers are reserved for things a browser form cannot do: webhooks, cron, signed downloads, uploads.revalidatePath for cache coherence."use server" file may only export async functions, so constants and enums live in sibling _constants/ modules.| Client | Key | Respects RLS | Used for |
|---|---|---|---|
createClient() (server, cookies) | publishable / anon | Yes | Default for reads and most writes |
createClient() (browser) | publishable / anon | Yes | Auth UI and light client interactions |
createAdminClient() | secret / service role | No | Auth admin, cron, webhooks, selected writes behind a guard |
The admin client is server-only, documented as dangerous at its definition, and every call site is expected to sit behind a requireXAdmin() guard. Env access is centralised in env.ts, which also accepts both the new (publishable / secret) and legacy (anon / service_role) Supabase key names so the app survives the platform's key migration.
npm run migrate:test, verify behaviour and RLS, then npm run migrate:live. The migration script knows both project refs and refuses to guess.NEW row and forbids setting any lifecycle field, so a visitor can create but never read, update or escalate:CREATE POLICY "proposals_public_insert" ON public.proposals
FOR INSERT TO anon, authenticated
WITH CHECK (
status = 'NEW'
AND lead_id IS NULL
AND reviewed_by IS NULL
AND reviewed_at IS NULL
AND internal_note IS NULL
);
Every cron endpoint (/api/hrm/cron/*, /api/billing/cron/run) is protected by a shared secret (X-CRON-SECRET or Vercel's bearer token) and is safe to run twice. Weeks are created atomically and de-duplicated; invoices are de-duplicated on (schedule_id, due_on); reminders and warnings are recorded so a retried run sends nothing new. See Section 9.
Order fulfilment is driven only by an HMAC-SHA256-verified webhook from SMS Pay, compared with crypto.timingSafeEqual over the raw request body. The browser redirect is never trusted as proof of payment, and the /api/checkout/confirm route is a read-only status check that exists purely so the checkout page can show progress. The subscription system adds a second, separate webhook endpoint (mapping inv_<id> back to an invoice through a unique payment-link intent id) so the original checkout path stayed untouched while billing was added.
The local gateway has no auto-debit. Rather than fake it, billing is a daily idempotent cycle that materialises invoices, issues a 14-day payment link, emails it, reminds at +3 and +7 days, and marks overdue. It is simple, auditable, and does not require storing payment credentials.
Locale (bn / en) is stored in a NEXT_LOCALE cookie and resolved server-side by next-intl. English is the default; Bangla is one click away. This avoided restructuring every route under /[locale] and keeps auth callback URLs stable. Only the public site is translated today; dashboards remain English by design (the staff working language).
AGENTS.md is the single source of truth for contributors and AI agents, with a required reading order (CRITICAL_SYSTEMS → skills → module docs → stack docs → code) and an explicit list of high-risk areas (proxy.ts, app/utils/auth/*, app/utils/supabase/*, app/auth/*, cron routes, payment flows). docs/ is kept as a durable reference set, not a task log.
| # | Decision | Primary benefit | Accepted cost |
|---|---|---|---|
| 001 | Modular monolith | One deploy, low ops | Soft boundaries |
| 002 | One identity + per-module profile tables | SSO and isolation | Denormalised email, multi-table lookup |
| 003 | Education > CRM > HRM > Store routing | Deterministic landing | Rule duplicated in two places |
| 004 | Single role resolver | One place to reason about access | Hot path, several queries |
| 005 | Proxy as coarse gate only | Framework-aligned, fails closed | Must remember layout guards |
| 006 | Actions for people, routes for machines | Typed, small attack surface | Convention to maintain |
| 007 | Privileged client is opt-in | Least privilege by default | Discipline at call sites |
| 008 | RLS + forward-only migrations | Safe on live data | Slower schema change |
| 009 | Idempotent secret-protected cron | Safe retries | Extra bookkeeping columns |
| 010 | HMAC-verified webhook | Tamper-proof payment state | Webhook setup per environment |
| 011 | Scheduled payment links | Works with the local gateway | Not fully automatic |
| 012 | Cookie-based locale | Stable URLs, small change | Not shareable by URL |
| 013 | Docs and agent rules as architecture | Safe AI-assisted changes | Docs must be curated |
The returned object is deliberately flat and boring:
interface UserWithRoles {
userId: string;
email: string;
profileRole: "customer" | "admin" | null;
crmRole: "ADMIN" | "MARKETER" | null;
hrmRole: "SUPER_ADMIN" | "ADMIN" | "EMPLOYEE" | null;
storeRole: "USER" | "ADMIN" | null;
hasEducationAccess: boolean;
hasCrmAccess: boolean;
hasHrmAccess: boolean;
hasStoreAccess: boolean;
storePermissions: StorePermission[]; // owner_purchase_manage, balance_add, ...
storeCapabilities: StoreCapabilities; // role AND permission, pre-computed
canAccessCrmAdmin: boolean;
canActAsCrmMarketer: boolean;
}
| Capability | Customer | CRM Marketer | CRM Admin | HRM Employee | HRM Admin | HRM Super | Store User | Store Admin* |
|---|---|---|---|---|---|---|---|---|
| Buy courses, view own payments | ✅ | - | - | - | - | - | - | - |
| Work leads and deals | - | ✅ | ✅ | - | - | - | - | - |
| Manage CRM users, import, delete | - | - | ✅ | - | - | - | - | - |
| Submit weekly task reports | - | - | - | ✅ | ✅ | ✅ | - | - |
| Mark weekly reports | - | - | - | - | ✅ | ✅ | - | - |
| Manage departments, recruitment | - | - | - | - | - | ✅ | - | - |
| Buy from the office store | - | - | - | - | - | - | ✅ | ✅ |
| Manage stock, products, balances | - | - | - | - | - | - | - | ✅* |
* Store admins hold granular permissions (stock_manage, product_manage, invoice_manage, balance_add, owner_purchase_manage, permissions_manage). Being ADMIN is necessary but not sufficient.

| Layer | File(s) | Responsibility | Fails to |
|---|---|---|---|
| 1. Proxy | proxy.ts, app/utils/supabase/middleware.ts | Session refresh, anon redirect, app-home routing | /login |
| 2. Layout guard | app/dashboard/*/layout.tsx, app/utils/auth/require.ts | Module and sub-module access (requireHrmSuperAdmin, ...) | /unauthorized |
| 3. Action / route | app/**/_actions/*, app/api/**/route.ts | Re-check the caller, validate input, verify cron secrets and webhook signatures | 401 / 403 / error result |
| 4. Scoped client | app/utils/supabase/{server,admin}.ts | Least privilege: publishable key by default, secret key opt-in | RLS denies |
| 5. RLS | supabase/migrations/* | Row-level truth, independent of the app | zero rows |
CRM contracts live in a private Storage bucket. The download route resolves the caller, loads the document with the privileged client, then applies application-level policy that mirrors RLS: any CRM user, or the specific customer the current version was explicitly shared with. It returns a 5-minute signed URL, never a public link.
// app/api/documents/[id]/download/route.ts (excerpt)
const isCrm = roles.hasCrmAccess;
const isSharedCustomer =
doc.customer_profile_id === roles.userId &&
doc.shared_at !== null &&
doc.is_current;
if (!isCrm && !isSharedCustomer) {
return NextResponse.json({ error: "Forbidden" }, { status: 403 });
}
const { data: signed } = await supabase.storage
.from(doc.storage_bucket || "crm-documents")
.createSignedUrl(doc.storage_path, 60 * 5, { download: doc.file_name });
| Principle | How it shows up |
|---|---|
| Identity is external to domains | Every *_users PK is an FK to auth.users(id); deleting an auth user cascades |
| Prefer enums to free text | deal_stage, invoice_status, proposal_status, payment_link_status, document_kind |
| Append-only history for audits | deal_events timeline, intern application events, email delivery logs |
| Versioned documents | group_id + version + is_current instead of overwriting contracts |
| Constraints encode business rules | e.g. an HRM week key must equal its Friday date; unique (program, email) per applicant |
| Money-like data is ledger-derived | Store balances are computed from store_account_entries, never stored as a mutable number |
Every transition writes a row to deal_events (STAGE_CHANGED, NOTE, VALUE_CHANGED, OWNER_CHANGED, WON, LOST, REOPENED), so the timeline on the deal page is a read of an immutable log rather than a derived guess.
Applicants have no direct table access. The public form writes through a server action using the service role, protected by a unique (program, email) index and a best-effort rate limiter. Applicants follow progress with an unguessable tracking token, and internal stages that should stay private (waitlist, no-show, declined offer) are collapsed before they reach the applicant-facing view.
// lib/internships/status.ts (excerpt): 13 internal states become 7 applicant-facing stages
export function toApplicantStage(status: string): ApplicantStage {
switch (status) {
case "SUBMITTED":
return "received";
case "UNDER_REVIEW":
case "WAITLISTED":
return "review"; // a waitlist is never revealed
case "INTERVIEW_SCHEDULED":
case "INTERVIEWED":
return "interview";
case "SELECTED":
case "OFFER_ACCEPTED":
return "selected";
/* ... */
default:
return "closed"; // rejected, declined, no-show, ...
}
}
All 104 migrations apply cleanly on a fresh local Supabase stack (supabase start), which is also how the screenshots in this document were produced.
/internships/track/[token]), offer accept/decline (/internships/offer/[token]) and public certificate verification (/verify/intern/[code]).FINAL_STATUSES: completed, rejected, expired) before moving the order.Design notes worth calling out:
document with a kind and nullable contract_status, effective_date, expires_date, counterparty. Fewer joins, one upload path, one RLS policy.Three sub-systems share the module:
BONUS, APPRECIATION, IMPROVEMENT, FINE). All time logic is pinned to Asia/Dhaka, and week keys are validated to be the Friday date by a database CHECK constraint..ics invites and reminder emails), offers with expiry, and onboarding.jspdf, xlsx).store_users does not depend on HRM, even though admin delegation echoes the company hierarchy.All schedules are declared in vercel.json. Times are UTC; the business runs on Asia/Dhaka (UTC+6).
| Route | UTC | Dhaka | Purpose |
|---|---|---|---|
/api/hrm/cron/create-weekly-pending-reports | Fri 03:00 | Fri 09:00 | Atomically create the week and one pending report per active employee |
/api/hrm/cron/remind-weekly-report | Fri 03:05 | Fri 09:05 | Remind employees whose report is still pending |
/api/hrm/cron/compute-last-friday | Fri 17:30 | Fri 23:30 | Store score-only results and close the week |
/api/hrm/cron/intern-interview-reminders | Daily 03:00 | Daily 09:00 | Email reminders for interviews in the next 36 hours |
/api/hrm/cron/intern-daily | Daily 13:00 | Daily 19:00 | Absences, auto check-out, attendance warnings, expiry and cleanup |
/api/billing/cron/run | Daily 02:00 | Daily 08:00 | Materialise invoices, issue links, mark overdue, send reminders |
| Failure | Behaviour |
|---|---|
| SMTP is down | The database action still commits; the delivery is logged as FAILED and can be retried from the admin UI |
| Cron fires twice | No duplicate weeks, invoices, reports, reminders or warnings |
| Webhook replayed or forged | Signature check rejects forgeries; processing is keyed on the intent id |
| Role lookup fails in the proxy | Protected routes redirect to /login; public routes are unaffected |
| A module's table is missing (rollout) | Role resolver treats it as "no access" rather than throwing |
| Concern | Approach |
|---|---|
| Rendering | Server Components by default; Client Components only for interaction (forms, charts, tables) |
| Data loading | Fetch on the server, stream with 26 route-level loading.tsx skeletons; no client data-fetching library needed |
| Forms | React Hook Form + Zod resolvers; validation schemas shared in lib/validations |
| Design system | shadcn/ui on Radix primitives (30 components in components/ui), Tailwind CSS 4 with CSS variables |
| Theming | Yellow brand, dark-first dashboards with light-theme support; a test fails if hard-coded light colours creep back in |
| Tables and charts | TanStack Table, Recharts |
| Navigation | A shared DashboardShell with per-module sidebars, breadcrumbs, and a module switcher for multi-role users |
| Responsive | Mobile-first; dashboards collapse to a bottom navigation on phones |
| i18n | next-intl, cookie-driven, English default, Bangla available on the public site |
| PWA | Service-worker registration for installable behaviour |
Tests use the Node built-in runner (node --test) with tsx, so there is no test framework to maintain. The suites target domain logic (the part that is expensive to get wrong) rather than rendering.
| Suite | Tests | What it protects |
|---|---|---|
test:hrm | 56 | KPI flow, cron auth, HRM access/deactivation, recruitment, offers, emails, intern attendance and completion |
test:store | 15 | Ledger math, reporting, store domain rules |
test:crm | 6 | CRM domain rules |
test:edu | 5 | Service pricing |
test:landing | 5 | Public form validation |
test:ui | 1 | Theme-contrast guard (no hard-coded light colours in dark surfaces) |
| Total | 88 | All passing |
npm run test:all
Prettier (format:check), ESLint 9 with the Next config, TypeScript via next build, and unit tests run on every push and pull request.
scripts/seed-public.mjs, seed-crm.mjs, seed-hrm.mjs create test identities, leads, services, projects and payments; db:clean resets.verify:intern-db checks that the internship migrations are present on the target database.| Concern | Choice |
|---|---|
| Hosting | Vercel (serverless functions, cron, edge proxy) |
| Database | Supabase Postgres (managed), local stack for development |
| Observability | Vercel Analytics and Speed Insights; structured console.error tags in the proxy |
| Secrets | Environment variables; public vs server-only keys separated in env.ts |
| Build safety | CI builds with placeholder public env so type and route errors are caught without secrets |
All screenshots below were captured from the running application against a local Supabase stack seeded with test data (test accounts, no real customer data).

Landing. Server-rendered, English by default, Bangla switch in the header.

Proposal wizard. Four steps with React Hook Form and Zod, inserting with the anon key under a narrow RLS policy.

Internships. The public program page that feeds the recruitment pipeline.

One login for every module. The proxy decides where you land.

A single account with rows in several module tables sees all of them in the Switch Application dialog. The active module is highlighted, and modules a user has no row for do not appear. This is ADR-002 and ADR-003 made visible.

Customer dashboard. Active services, spend, pending payments and the package browser.

Subscriptions. The output of the daily billing cycle: paid and open invoices.

Education admin. Customers, revenue, orders and pending payments at a glance.

CRM dashboard. KPIs, trends, marketer performance and lead sources.

Leads. Ownership-based access: marketers work their own leads, admins see all.

Deals pipeline. Five open stages plus closed Won and Lost, with per-stage value totals.

Deal detail. Stage control, notes, and an append-only timeline backed by the deal_events table.

Proposal inbox. Public submissions land here for review and one-click conversion to a lead.

Super admin. People, departments, job roles, recruitment, weekly submissions and performance distribution.

Employee view. Monthly score, tier, history and trend: the same module with a very different capability set.

Recruitment. Program window, funnel counts and per-position vacancies.

Applications. Search, filter and CSV export across all pipeline states.

Application detail. Applicant data, interviews, status changes with timeline notes and structured reviewer scoring. The CV link is a short-lived signed URL.

Products. Catalogue, stock tracking and pricing, gated by the product_manage permission.

Reports. Sales by employee and product, low stock, negative balances, with CSV and PDF export.
Mobile-first layouts: the public site on a 390px phone, and a dashboard that switches to a bottom navigation bar.
<img src="https://www.wetraineducation.com/case-study/mobile-landing-home.png" alt="Public site on a phone" width="300" /> <img src="https://www.wetraineducation.com/case-study/mobile-hrm-super.png" alt="Dashboard with bottom navigation on a phone" width="300" />
A case study that only lists wins is a brochure. These are the known compromises, ranked roughly by how much I would like to fix them.
| Area | Debt | Why it exists | Planned fix |
|---|---|---|---|
| Authorization model | app/utils/auth/crm-capabilities.ts hard-codes a single "dual capability" admin UUID | A pragmatic stop-gap while CRM roles had only ADMIN and MARKETER | Replace with the RBAC redesign (roles, permissions, assignments) |
| Role resolution | The proxy and getCurrentUserWithRoles() each query the membership tables, the resolver's five lookups run sequentially, and the proxy does not check hrm_users.is_active (the layout guard does) | The proxy cannot share request-scoped state with server components | Run lookups with Promise.all, memoise per request, and share one routing function |
| Test depth | Tests cover domain logic, not RLS policies or end-to-end flows; CI runs only the HRM and Store suites | RLS testing needs a database per run; the other suites were added later | Run all suites in CI and add a Postgres-backed RLS test layer (PGlite is already a dev dependency) |
| Seed data | supabase/seed.sql uses a legacy 2026-W16 week key that the newer week-key CHECK constraint now rejects, so supabase db reset fails | Schema evolved faster than the seed | Regenerate the seed from the current schema and run it in CI |
| Rate limiting | The public internship limiter is in-memory, per instance | Cheap defence-in-depth; the unique index is the real guarantee | Move to a shared store (Redis / Upstash) |
| Validation | Zod is used heavily on public forms and offers, less uniformly inside internal actions | Internal actions sit behind guards and were written first | Introduce a shared action() wrapper that enforces auth + schema |
| Dependencies | stripe is still listed in package.json but is not imported anywhere | Left over from an earlier gateway choice | Remove it |
| i18n scope | Only the public site is translated | Staff working language is English | Extend namespaces module by module |
| Docs drift | Some older docs describe patterns (React Query, middleware.ts) that the code no longer uses | Early docs were aspirational | Curate docs/ against the code; the root rules file remains authoritative |
WITH CHECK that selects from the same table made a policy recurse infinitely; the fix was a dedicated migration and a rule to test policies against intended and unintended roles.CHECK that a week key equals its Friday date prevented an entire class of date bugs, and also caught my own stale seed file.AGENTS.md, the high-risk area list and the required reading order measurably reduced risky edits in auth and database code.# 1. install
npm install
# 2. start Postgres, Auth and Storage locally and apply all migrations
npx supabase start
# 3. configure the app (copy, then fill in the URL and keys that `supabase start` prints)
cp .env.example .env.local
# 4. seed test users and sample data (never run against production)
npm run seed:all
# 5. run the app
npm run dev
Open http://localhost:3000. Seed scripts create test identities such as crm.admin@seed.local and hrm.super@seed.local; see scripts/seed-*.mjs for the full list and the shared test password.
Note. The checked-in
supabase/seed.sqlis currently out of date with the newest migrations (see debt table). Ifsupabase startfails while seeding, set[db.seed] enabled = falseinsupabase/config.toml, start the stack, then use theseed:*scripts above.
| Command | What it does |
|---|---|
npm run dev | Start the Next.js dev server |
npm run build / start | Production build and server |
npm run lint | ESLint |
npm run format:check | Prettier check (used in CI) |
npm run test:all | All 88 tests |
npm run seed:all | Seed public content, CRM and HRM test data |
npm run db:clean | Reset seeded data |
npm run migrate:test | Apply migrations to the test project |
npm run migrate:live | Apply migrations to the live project (after verifying on test) |
AGENTS.md is the primary instruction file for agents; CLAUDE.md imports it..mcp.json / .vscode/mcp.json configure the Next.js DevTools MCP and the Supabase MCP. Use them against local or non-production environments only.skills/.app/
├── (landing)/ # public marketing, proposals, internships, verification
├── dashboard/
│ ├── customer/ admin/ # Education
│ ├── crm/ # CRM (_actions, _components, _constants, _types)
│ ├── hrm/ # HRM (super | admin | employee, recruitment, internship)
│ └── store/ # Store (_actions, _components, _lib)
├── api/ # webhooks, cron, uploads, signed downloads
├── auth/ # unified callback, invite, magic link, email change
├── actions/ # public server actions (proposal, internship, locale)
└── utils/
├── auth/ # roles.ts, require.ts, hrm-access.ts, crm-capabilities.ts
└── supabase/ # client, server, admin, middleware, env
components/ # ui/ (shadcn), shared/, hrm/, skeletons/
lib/ # billing/, hrm/, internships/, validations/
i18n/ messages/ # next-intl config and en / bn catalogues
supabase/migrations/ # 104 forward-only migrations
scripts/ # seed, migrate, verify, clean
tests/ # crm, education, hrm, store, landing, ui
docs/ skills/ AGENTS.md # durable references and agent rules
proxy.ts # Next.js 16 proxy (session refresh + coarse gate)
vercel.json # cron schedules
public/case-study/ # screenshots and diagrams used in this README
AGENTS.md: primary contributor and agent rulesdocs/README.md: map of the durable documentation setdocs/ARCHITECTURE_DECISION_USERS_PROFILES.md: the original long-form ADR for ADR-002docs/CRITICAL_SYSTEMS.md: systems that must not regressdocs/HRM_CRON.md: scheduled HRM logicdocs/DATABASE_RULES.md and docs/DATABASE_MIGRATION_SAFETY.md: database safetyBuilt with Next.js, Supabase and a lot of care about who is allowed to see what.

E-commerce Admin Panel
React · Tailwind · Node.js · Express

Portfolio Admin Panel
React · Tailwind · Node.js · Express

A secure Bangla-first mobile financial service simulation built with an auditable double-entry ledger
Next.js 16 · React 19 · TypeScript · Tailwind CSS v4