---
title: "Getting Started"
url: https://docs.growamal.com/en/getting-started/
language: en
---

# 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. The server defaults to port **8787** (set the `PORT` env var to
  override). Replace `http://localhost:8787` below with its address. The Next.js frontend runs on
  port 3000 -- do not confuse the two.
- 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):

```bash
curl -X POST http://localhost:8787/api/auth/register \
  -H "Content-Type: application/json" \
  -d '{
    "email": "admin@school.edu.jo",
    "password": "your-strong-password",
    "fullNameAr": "أحمد العلي",
    "primaryPersona": "ADMIN"
  }'
```

Response: `201 Created` (`RegisterResponse`):

```json
{
  "user": {
    "id": 1,
    "email": "admin@school.edu.jo",
    "primaryPersona": "ADMIN",
    "organizationId": null
  },
  "tokens": {
    "accessToken": "eyJhbGci...",
    "refreshToken": "9f3c...e1",
    "expiresIn": 900,
    "tokenType": "Bearer"
  }
}
```

> **Warning:** 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](https://docs.growamal.com/en/guides/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:

```bash
curl -X POST http://localhost:8787/api/auth/login \
  -H "Content-Type: application/json" \
  -d '{
    "email": "admin@school.edu.jo",
    "password": "your-strong-password"
  }'
```

```ts
const res = await fetch("http://localhost:8787/api/auth/login", {
  method: "POST",
  headers: { "Content-Type": "application/json" },
  body: JSON.stringify({
    email: "admin@school.edu.jo",
    password: "your-strong-password",
  }),
});

if (!res.ok) {
  // Error envelope: { code, message, details?, retryAfter? }
  const err = await res.json();
  throw new Error(`${err.code}: ${err.message}`);
}

const { accessToken, refreshToken, expiresIn } = (await res.json()) as {
  accessToken: string;
  refreshToken: string;
  expiresIn: number;
  tokenType: "Bearer";
};
```

Response: `200 OK` (`AuthTokensResponse`):

```json
{
  "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:

```bash
curl http://localhost:8787/api/auth/me \
  -H "Authorization: Bearer eyJhbGci..."
```

Response: `200 OK` (`MeResponse`, abbreviated):

```json
{
  "id": 1,
  "email": "admin@school.edu.jo",
  "primaryPersona": "ADMIN",
  "organizationId": null,
  "permissions": ["SUPER_ADMIN", "MANAGE_ORG", "..."],
  "classIds": []
}
```

`permissions[]` is what actually gates endpoints. See [Core Concepts → RBAC](https://docs.growamal.com/en/core-concepts/).

## 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.

```bash
# Organization
curl -X POST http://localhost:8787/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`.

```bash
# School (organizationId = the org's id from above)
curl -X POST http://localhost:8787/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 http://localhost:8787/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`:

```bash
curl -X POST http://localhost:8787/api/users \
  -H "Authorization: Bearer eyJhbGci..." \
  -H "Content-Type: application/json" \
  -d '{
    "primaryPersona": "TEACHER",
    "email": "teacher@school.edu.jo",
    "password": "teacher-strong-password",
    "fullNameAr": "سارة محمود",
    "organizationId": "clx4z2k0u0000xyz1234abcde"
  }'
```

The `201 UserCreatedResponse` returns the new user's integer `id`. See
[Provision Users](https://docs.growamal.com/en/guides/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:

```bash
curl -X POST http://localhost:8787/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`:

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

See [Errors](https://docs.growamal.com/en/errors/) for the full status + code catalog and retry guidance.
