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/serverinstance. Replacehttps://localhost:3000below with its address. - A terminal with
curl(and optionallyjqto 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
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.