User Account API
Rocky currently exposes two user-identity APIs: register (/api/register) and login (/api/auth). Both are Next.js BFF routes that write to the Postgres auth.users table and call the Canton Validator to allocate a party.
Token model uses Canton JWTs signed by the Validator. Read Canton Network first if you're unfamiliar with Party / Validator concepts.
1. Overview
| Dimension | Current implementation |
|---|---|
| Auth model | Email + password (bcrypt hash) |
| Token type | Canton JWT (signed by the Validator, bound to a party) |
| Session storage | Browser localStorage |
| User table | auth.users (Postgres) |
| Party allocation | DevNet: Validator /v0/admin/users. LocalNet: Daml admin API |
Rocky's auth.users row corresponds 1:1 with a Canton Party. Registration in one shot:
- Validates email / password / username
- Calls the Validator to allocate a Canton Party (hint = username)
- INSERTs into
auth.users(with bcrypt password hash) - Returns Canton JWT + party + email + user_id
All identity-requiring endpoints later on (perp order submit, MTC claim, etc.) verify either user_id or party at the BFF layer.
2. POST /api/register
Create a new Rocky account.
2.1 Request
POST /api/register
Content-Type: application/json
{
"email": "alice@example.com",
"password": "alice-secret-123",
"username": "alice"
}
| Field | Type | Required | Rule |
|---|---|---|---|
email | string | yes | Matches /^[^\s@]+@[^\s@]+\.[^\s@]+$/. Case-insensitive (stored as entered; compared via lower(email)) |
password | string | yes | Length ≥ 8. No character-complexity requirement (DevNet simplification) |
username | string | yes | Matches /^[a-zA-Z0-9_-]{3,32}$/. Used as the Canton party hint |
2.2 Response — 200 OK
{
"success": true,
"token": "eyJhbGciOiJSUzI1NiIs...",
"party": "alice::1220ca15423a1b049bf8e84132360f246057aa18f5e9e36a77db535b75bd287cceff",
"username": "alice",
"email": "alice@example.com",
"user_id": "df94d294-5319-49d6-ba8d-aa08094bc1e4"
}
| Field | Meaning |
|---|---|
token | Canton JWT. Used for endpoints that need user-act-as authority |
party | Full Canton party id (prefix = username, suffix = participant fingerprint) |
user_id | auth.users.user_id primary key (UUIDv4); every perp endpoint's user_id parameter uses this |
2.3 Response — errors
| HTTP | error message | When |
|---|---|---|
400 | "valid email is required" | email missing / malformed |
400 | "password must be at least 8 characters" | password missing / < 8 chars |
400 | "username must be 3-32 characters (letters, numbers, underscore, hyphen)" | username missing / regex mismatch |
409 | "an account with this email already exists" | email already taken (case-insensitive) |
409 | "username is already taken" | username already taken |
500 | <canton validator error> | Validator party allocation failed (DevNet) / Daml admin API failed (LocalNet) |
2.4 Example
curl -s -X POST https://demo.rocky.exchange/api/register \
-H 'Content-Type: application/json' \
-d '{
"email": "alice@example.com",
"password": "alice-secret-123",
"username": "alice"
}' | jq
2.5 Side effects
After one successful call:
- A new row in
auth.users - A new party on Canton;
UUIDv5(party)is how the user_id appears in the perp system (getPerpUserId()on the frontend) - The user can immediately log in, place orders, deposit CC, trigger MTC rewards
3. POST /api/auth
Exchange email + password for a Canton JWT.
3.1 Request
POST /api/auth
Content-Type: application/json
{
"email": "alice@example.com",
"password": "alice-secret-123"
}
| Field | Type | Required | Rule |
|---|---|---|---|
email | string | yes | Case-insensitive, queried via lower(email) |
password | string | yes | bcrypt-compared against the stored hash |
3.2 Response — 200 OK
{
"token": "eyJhbGciOiJSUzI1NiIs...",
"party": "alice::1220ca15423a1b049bf8e84132360f246057aa18f5e9e36a77db535b75bd287cceff",
"username": "alice",
"email": "alice@example.com",
"user_id": "df94d294-5319-49d6-ba8d-aa08094bc1e4"
}
Note: unlike /api/register, the login response has no success: true field (early convention difference; will be unified later).
3.3 Response — errors
| HTTP | error message | When |
|---|---|---|
400 | "email is required" | email missing |
400 | "password is required" | password missing |
401 | "invalid email or password" | email doesn't exist OR password is wrong (same error, deliberately not distinguished to avoid email enumeration) |
500 | <canton authToken error> | Validator JWT signing failed |
3.4 Example
curl -s -X POST https://demo.rocky.exchange/api/auth \
-H 'Content-Type: application/json' \
-d '{
"email": "alice@example.com",
"password": "alice-secret-123"
}' | jq
3.5 Security notes
- The error is uniformly
"invalid email or password"— do not leak whether the email exists. Clients should not differentiate by status code. - No rate limiting / IP lockout right now (DevNet simplification); production needs both.
- Passwords are stored as bcrypt hashes (10 rounds), never returned, never logged.
4. Tokens and localStorage
Successful login / register writes 4 values to browser localStorage:
| key | value | Purpose |
|---|---|---|
mtc_token | Canton JWT | Attached to on-chain API calls |
mtc_party | Full Canton party id | user-act-as in on-chain contract calls |
mtc_username | username | Display |
mtc_email | Display |
The
mtc_*prefix is a legacy from the old brand (MTC = Mining Token Credit); not renamed to avoid breaking existing sessions.
4.1 Logout
No dedicated logout API. The frontend just clears the 4 localStorage items + navigates to /. The token remains valid on Canton until it naturally expires (typically 1 hour), but the local copy is gone.
// TopNav.tsx — handleLogout
localStorage.removeItem("mtc_token");
localStorage.removeItem("mtc_party");
localStorage.removeItem("mtc_username");
localStorage.removeItem("mtc_email");
router.push("/");
4.2 Token refresh
A Canton JWT naturally expires; the server then rejects the user. No refresh-token mechanism currently — the user must re-login. Planned improvements:
- Add
POST /api/auth/refresh: exchange old token + user_id for a new one - Or extend token lifetime to 7 days and re-verify on sensitive operations
5. Storage
5.1 auth.users schema
See services/internal-ledger/migrations/20260525002_auth_users.sql:
CREATE TABLE auth.users (
user_id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
email TEXT NOT NULL UNIQUE,
password_hash TEXT NOT NULL,
username TEXT NOT NULL UNIQUE,
party TEXT NOT NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
CREATE INDEX users_email_lower_idx ON auth.users (lower(email));
5.2 Relationship to auth.api_keys
auth.api_keys is a different table, used by the funnel market-making bot (HMAC keys). The two are fully independent:
auth.users— web login usersauth.api_keys— API keys for programmatic trading
They don't interact.
6. Planned endpoints
The following are not yet implemented; planning only:
| Endpoint | Method | Purpose |
|---|---|---|
/api/auth/me | GET | Exchange token for current user info (used to restore session on page reload) |
/api/auth/refresh | POST | Exchange old token for new, extending the session |
/api/auth/password | PUT | Change password (requires old password) |
/api/auth/email | PUT | Change email (requires re-verifying ownership of new email → needs SMTP) |
/api/auth/forgot-password | POST | "Forgot password" flow (needs SMTP for the reset link) |
/api/auth/logout | POST | Server-side token invalidation (needs token revocation list) |
/api/admin/users | GET / DELETE | Admin backstage: list / disable users |
Implementation timing depends on SMTP integration (password reset) + token revocation mechanism (admin disable / true logout).
7. Related docs
- Canton Network — Party / Validator concepts
- CC Deposit Flow — how user_id is used in perp endpoints
- MTC Mining Reward — how token + party are used in the mining flow
- Repo source:
mtc-exchange/src/app/api/register/route.tsmtc-exchange/src/app/api/auth/route.tsmtc-exchange/src/lib/users.tsmtc-exchange/src/lib/passwords.tsservices/internal-ledger/migrations/20260525002_auth_users.sql