Authentication
Wallet-based auth, JWT tokens, and refresh flow
Vezta uses wallet-based authentication supporting both Solana and EVM chains. Instead of passwords, users sign a message with their wallet to prove ownership, and the backend issues JWT tokens.
Auth Flow
- Request nonce -- Client sends wallet address and chain, backend returns a unique nonce
- Sign message -- Client signs the nonce with their wallet (Phantom, MetaMask, etc.)
- Verify signature -- Backend verifies the signature, creates or finds the user, and issues tokens
- Use access token -- Client includes the JWT in the
Authorization: Bearerheader for subsequent requests - Refresh when expired -- Access tokens expire after 15 minutes; use the refresh endpoint to get a new one
Endpoints
| Method | Path | Auth | Description |
|---|---|---|---|
| POST | /api/v1/auth/nonce | Public | Request a nonce for wallet signature |
| POST | /api/v1/auth/verify | Public | Verify wallet signature and receive JWT |
| POST | /api/v1/auth/refresh | Public | Refresh an expired access token |
| POST | /api/v1/auth/validate-key | Public | Validate a beta access key |
| POST | /api/v1/auth/logout | Bearer | Logout and invalidate all refresh tokens |
Request Nonce
Generate a nonce that the user must sign with their wallet.
// POST /api/v1/auth/nonce
{
"walletAddress": "7xKXtg2CW87d97TXJSDpbD5jBkheTqA83TZRuJosgAsU",
"chain": "solana"
}// Response 201
{
"nonce": "vezta-auth-1711234567890-abc123def456"
}Verify Wallet
Submit the signed nonce to authenticate. On success, the backend returns an access token in the response body and sets an httpOnly refresh_token cookie. A non-httpOnly logged_in=1 marker cookie is also set for frontend route protection.
// POST /api/v1/auth/verify
{
"walletAddress": "7xKXtg2CW87d97TXJSDpbD5jBkheTqA83TZRuJosgAsU",
"chain": "solana",
"signature": "3ASDf...base58signature",
"nonce": "vezta-auth-1711234567890-abc123def456",
"referralCode": "FRIEND123"
}// Response 201
{
"accessToken": "eyJhbGciOiJIUzI1NiIs...",
"user": {
"id": "clxyz...",
"walletAddress": "7xKXtg2CW87d97TXJSDpbD5jBkheTqA83TZRuJosgAsU",
"chain": "solana",
"username": null,
"avatarUrl": null
}
}Refresh Token
Exchange a refresh token for a new access token. The backend accepts the refresh token either in the request body or from the httpOnly cookie. Token rotation is applied -- each refresh issues a new refresh token and invalidates the old one.
// POST /api/v1/auth/refresh
{
"refreshToken": "previous-refresh-token-value"
}// Response 200
{
"accessToken": "eyJhbGciOiJIUzI1NiIs...",
"refreshToken": "new-refresh-token-value",
"user": {
"id": "clxyz...",
"walletAddress": "7xKXtg2CW87d97TXJSDpbD5jBkheTqA83TZRuJosgAsU",
"chain": "solana"
}
}Token Details
- Access token: JWT, 15-minute expiry, stored in memory (not localStorage)
- Refresh token: Opaque token, 7-day expiry, stored as httpOnly cookie with
SameSite=NoneandSecure=truein production - JWT payload:
{ id, walletAddress, chain }
Notes
- The
referralCodefield in the verify request is optional and only used on first-time sign-up - The
chainfield acceptssolanaorevm - All auth endpoints except
logoutare public (no JWT required) - Failed signature verification returns a
401 Unauthorizederror