Skip to main content

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

DimensionCurrent implementation
Auth modelEmail + password (bcrypt hash)
Token typeCanton JWT (signed by the Validator, bound to a party)
Session storageBrowser localStorage
User tableauth.users (Postgres)
Party allocationDevNet: Validator /v0/admin/users. LocalNet: Daml admin API

Rocky's auth.users row corresponds 1:1 with a Canton Party. Registration in one shot:

  1. Validates email / password / username
  2. Calls the Validator to allocate a Canton Party (hint = username)
  3. INSERTs into auth.users (with bcrypt password hash)
  4. 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"
}
FieldTypeRequiredRule
emailstringyesMatches /^[^\s@]+@[^\s@]+\.[^\s@]+$/. Case-insensitive (stored as entered; compared via lower(email))
passwordstringyesLength ≥ 8. No character-complexity requirement (DevNet simplification)
usernamestringyesMatches /^[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"
}
FieldMeaning
tokenCanton JWT. Used for endpoints that need user-act-as authority
partyFull Canton party id (prefix = username, suffix = participant fingerprint)
user_idauth.users.user_id primary key (UUIDv4); every perp endpoint's user_id parameter uses this

2.3 Response — errors

HTTPerror messageWhen
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"
}
FieldTypeRequiredRule
emailstringyesCase-insensitive, queried via lower(email)
passwordstringyesbcrypt-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

HTTPerror messageWhen
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:

keyvaluePurpose
mtc_tokenCanton JWTAttached to on-chain API calls
mtc_partyFull Canton party iduser-act-as in on-chain contract calls
mtc_usernameusernameDisplay
mtc_emailemailDisplay

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 users
  • auth.api_keys — API keys for programmatic trading

They don't interact.


6. Planned endpoints

The following are not yet implemented; planning only:

EndpointMethodPurpose
/api/auth/meGETExchange token for current user info (used to restore session on page reload)
/api/auth/refreshPOSTExchange old token for new, extending the session
/api/auth/passwordPUTChange password (requires old password)
/api/auth/emailPUTChange email (requires re-verifying ownership of new email → needs SMTP)
/api/auth/forgot-passwordPOST"Forgot password" flow (needs SMTP for the reset link)
/api/auth/logoutPOSTServer-side token invalidation (needs token revocation list)
/api/admin/usersGET / DELETEAdmin backstage: list / disable users

Implementation timing depends on SMTP integration (password reset) + token revocation mechanism (admin disable / true logout).


  • 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.ts
    • mtc-exchange/src/app/api/auth/route.ts
    • mtc-exchange/src/lib/users.ts
    • mtc-exchange/src/lib/passwords.ts
    • services/internal-ledger/migrations/20260525002_auth_users.sql