Skip to Content

Getting Started

This walkthrough takes you from a brand-new server to your first authenticated call. Every step is a real request against a running apps/server. Run them top to bottom and you will have a bootstrap admin, a token, a tenant hierarchy, and a provisioned user.

Prerequisites

  • A running apps/server instance. Replace https://localhost:3000 below with its address.
  • A terminal with curl (and optionally jq to pretty-print JSON).
  • Node 22 only matters for building the server, not for calling it.

Step 1: Register the bootstrap admin

Registration is open for the first user only. Register a SUPER-scope admin with no organizationId (the one and only case where it is omitted):

curl -X POST https://localhost:3000/api/auth/register \ -H "Content-Type: application/json" \ -d '{ "email": "[email protected]", "password": "your-strong-password", "fullNameAr": "أحمد العلي", "primaryPersona": "ADMIN" }'

Response: 201 Created (RegisterResponse):

{ "user": { "id": 1, "email": "[email protected]", "primaryPersona": "ADMIN", "organizationId": null }, "tokens": { "accessToken": "eyJhbGci...", "refreshToken": "9f3c...e1", "expiresIn": 900, "tokenType": "Bearer" } }

Registration closes after the first user. A second POST /api/auth/register returns 403 with code: "PERMISSION_DENIED" (“registration is closed”). From then on, every other user is created through Provision Users.

You already have a token pair from the response above, so you can skip Step 2 and reuse it.

Step 2: Log in

To get a fresh token pair at any time, log in with email + password:

curl -X POST https://localhost:3000/api/auth/login \ -H "Content-Type: application/json" \ -d '{ "email": "[email protected]", "password": "your-strong-password" }'

Response: 200 OK (AuthTokensResponse):

{ "accessToken": "eyJhbGci...", "refreshToken": "9f3c...e1", "expiresIn": 900, "tokenType": "Bearer" }

expiresIn is the access-token lifetime in seconds (900 = 15 minutes).

Step 3: Make your first authenticated call

Pass the access token as a Bearer header. GET /api/auth/me confirms your identity and returns your permission set:

curl https://localhost:3000/api/auth/me \ -H "Authorization: Bearer eyJhbGci..."

Response: 200 OK (MeResponse, abbreviated):

{ "id": 1, "email": "[email protected]", "primaryPersona": "ADMIN", "organizationId": null, "permissions": ["SUPER_ADMIN", "MANAGE_ORG", "..."], "classIds": [] }

permissions[] is what actually gates endpoints. See Core Concepts → RBAC.

Step 4: Create the tenant hierarchy

The bootstrap admin holds SUPER_ADMIN, so it can create the top of the tree. Create an organization, then a school under it, then a class under the school.

# Organization curl -X POST https://localhost:3000/api/organizations \ -H "Authorization: Bearer eyJhbGci..." \ -H "Content-Type: application/json" \ -d '{ "name": "مدرسة النور الأساسية", "slug": "al-noor-school-jo", "country": "JO", "defaultLocale": "ar-JO" }'

The 201 OrgResponse returns an id (a cuid string, e.g. clx4z2k0u0000xyz1234abcde). Use it as organizationId for the school. A school’s country must match its organization’s; a mismatch returns 422.

# School (organizationId = the org's id from above) curl -X POST https://localhost:3000/api/schools \ -H "Authorization: Bearer eyJhbGci..." \ -H "Content-Type: application/json" \ -d '{ "organizationId": "clx4z2k0u0000xyz1234abcde", "name": "مدرسة النور — عمان", "country": "JO" }' # Class (schoolId = the school's id; grade 1–4; academicYear "YYYY-YYYY") curl -X POST https://localhost:3000/api/classes \ -H "Authorization: Bearer eyJhbGci..." \ -H "Content-Type: application/json" \ -d '{ "schoolId": "clx4z2k0u0000xyz1234abcde", "name": "صف ثاني — أ", "grade": 2, "academicYear": "2026-2027" }'

Step 5: Provision a user

All non-bootstrap users are created through one endpoint, POST /api/users, whose body is a discriminated union keyed on primaryPersona:

curl -X POST https://localhost:3000/api/users \ -H "Authorization: Bearer eyJhbGci..." \ -H "Content-Type: application/json" \ -d '{ "primaryPersona": "TEACHER", "email": "[email protected]", "password": "teacher-strong-password", "fullNameAr": "سارة محمود", "organizationId": "clx4z2k0u0000xyz1234abcde" }'

The 201 UserCreatedResponse returns the new user’s integer id. See Provision Users for the full per-persona field matrix.

Step 6: Refresh your token when it expires

Access tokens live 15 minutes. When one expires, exchange the refresh token for a new pair:

curl -X POST https://localhost:3000/api/auth/refresh \ -H "Content-Type: application/json" \ -d '{ "refreshToken": "9f3c...e1" }'

Refresh tokens rotate: the refresh token you just used is now dead, and the response carries a new one. Reusing a rotated refresh token returns 401 and revokes the session (theft detection).

When things go wrong

Every failure is the same envelope: { code, message, details?, retryAfter? }. For example, a bad login password returns 401:

{ "code": "...", "message": "Invalid email or password." }

See Errors for the full status + code catalog and retry guidance.

Last updated on