Appearance
User Service + Auth
Status: in implementation (v1)
Related docs: High-Level System Design, Terminal Integration, User/Auth Module, Implementation Roadmap
Scope
UserService + Auth owns Pump Party user identity, wallet proof, app sessions/JWTs, linked wallets, and ownership checks.
Other services should authorize against backend userId, not wallet addresses.
Terminal is not app auth. Terminal profile linking and points display live in Terminal Integration.
Decision
Pump Party auth uses SIWE wallet login for v1, scoped to MegaETH wallets.
- Privy may be used as a wallet/signing UX, for example an embedded wallet created after social login.
- Do not use Privy JWTs, Privy user ids, or
privy-tokenas Pump Party app auth. - The backend owns the challenge, nonce, signature verification, user resolution, and app session.
- Exchange verified wallet identity for a Pump Party-owned access JWT plus a server-stored refresh token.
- Do not integrate MOSS until the product, wallet support, and verification contract are confirmed.
Standards
Use established wallet-login standards:
- EVM: Sign-In with Ethereum,
ERC-4361. - Smart contract wallets: out of scope for v1. The verification interface stays open so EIP-1271 (and EIP-6492 for undeployed accounts) can be added later without changing call sites.
- Multi-chain later: use chain-specific Sign-In standards or
CAIP-122style semantics.
Do not invent a custom wallet-login message format for EVM.
Chain Scope (v1)
- MegaETH mainnet (
chainId 4326) and MegaETH testnet (chainId 6343) only. - Both chain IDs are accepted; the verifier must reject any other chain.
- Allowed chain IDs are configurable via env so prod can narrow to mainnet-only without code changes.
Wallet Verification
V1 supports two verification paths, both branched on signature shape:
text
65-byte ECDSA signature (132 hex chars + "0x")
-> EOA personal_sign verification via viem `verifyMessage`
other signature shapes
-> MegaETH Porto Relay verification via `RelayActions.verifySignature`The Porto Relay path covers MegaETH-native wallet flows (e.g. Porto session keys). EIP-1271 is not implemented in v1. The internal verifier returns the same VerifiedWalletIdentity regardless of which path validated the signature, so callers do not branch.
Wallet Cardinality (v1)
One user has exactly one verified wallet for v1.
- The first SIWE login creates the user and stores the wallet as primary.
- Wallet linking endpoints are not exposed in v1.
- The schema keeps
user_walletsopen to a future many-to-one relationship without migration.
Login Flow
text
Frontend obtains an EVM signer
-> injected wallet, WalletConnect/Reown, Porto, or Privy embedded wallet
-> UserService issues SIWE challenge with nonce, domain, chainId, expiry
-> wallet signs SIWE message
-> frontend sends message and signature to UserService
-> UserService verifies domain, nonce, chain, expiry, and signature (EOA or Porto)
-> UserService resolves or creates user and wallet
-> UserService issues access JWT and refresh token, opens a session rowThe SIWE signature proves wallet ownership. The access JWT proves authentication to Pump Party for short-lived API calls. The refresh token rotates new access JWTs without re-prompting the wallet.
Privy is not a backend identity authority in this model. If a user signs through a Privy embedded wallet, the backend still sees only a SIWE message, signature, address, and chain id.
Session Model
Decision: session-bound access JWT plus opaque refresh token, with refresh rotation and DB-backed revocation.
- Access token: signed JWT (
HS256), TTL 24 hours. Carriessub = userId,sid = sessionId,iss,aud,iat,exp. Stateless verification on every request. - Refresh token: 32-byte cryptographically random opaque string, returned to the client once. Only the SHA-256 hash is stored server-side. TTL 14 days.
- Sessions table: one row per refresh token with
family_id,refresh_token_hash,expires_at,revoked_at,replaced_by_session_id, audit columns. - Rotation: a refresh request marks the current row revoked, inserts a new row with the same
family_id, pointsreplaced_by_session_idat the new row, and returns a new access JWT plus new refresh token. - Reuse detection: if a refresh token presents but its row is already revoked or replaced, the entire
family_idis revoked. This protects against stolen refresh tokens. - Revocation: explicit logout revokes the current session row (and optionally the entire user). Admins can revoke any session row directly in the database for emergency response.
JWT verification trusts the signature plus expiry; it does not look up the database for the access path. The refresh path always touches the database.
Frontend Packages
Use normal EVM wallet connection/signing tooling:
- injected wallet support for desktop wallets such as MetaMask and Rabby.
- WalletConnect/Reown if mobile wallet support is needed.
- Privy embedded wallets if the product wants social-login wallet creation.
- wagmi only if the frontend wants a React wallet abstraction across connectors.
- Porto wallet flows for MegaETH-native session-keyed signing.
The frontend package choice should not change the backend auth model. UserService verifies SIWE and issues the Pump Party session.
The current frontend Privy integration is only partially aligned: it wraps the app in PrivyProvider and uses Privy login/token state as app auth. The intended follow-up is to keep Privy only where it provides the wallet/signing UX, then submit the signed SIWE message to UserService and store/use Pump Party tokens for app authorization.
Future MOSS Integration
MOSS can be added later as another wallet proof provider if MegaETH confirms the product surface, supported wallets, and server verification contract.
text
siwe proof
-> message, signature, address, chainId
future_moss proof
-> provider token/JWT, origin, verified wallet addressBoth proof types should resolve to the same internal result:
text
VerifiedWalletIdentity
-> address
-> chainId
-> walletProviderProduct services should remain unchanged because they authorize against Pump Party userId, not provider-specific wallet proofs.
Data Model
Use stable backend identity:
text
users
id
email
display_name
created_at
updated_at
user_wallets
user_id
chain_namespace -- 'evm' for v1
chain_id -- 4326 (MegaETH mainnet) or 6343 (MegaETH testnet)
address -- lowercase hex
wallet_provider -- 'megaeth_eoa' | 'megaeth_porto'
verified_at
is_primary
created_at
updated_at
auth_challenges
nonce -- primary key
address
chain_id
domain
statement
uri
issued_at
expires_at
consumed_at
created_at
sessions
id -- session/refresh row id, surfaces as JWT `sid`
user_id
family_id -- shared across rotation chain
refresh_token_hash -- sha-256 hex of opaque refresh token
issued_at
last_used_at
expires_at
revoked_at
replaced_by_session_id
user_agent
ip_address
created_atWallet address is a credential, not the primary user key.
Provider tokens, if added later, are proofs or exchange artifacts, not product identity.
Endpoints
text
POST /api/v1/auth/siwe/challenge
POST /api/v1/auth/siwe/verify
POST /api/v1/auth/session/refresh
DELETE /api/v1/auth/session
GET /api/v1/meAuthenticated routes accept Authorization: Bearer <accessToken>. The dev-only x-user-id shortcut is gated behind both NODE_ENV !== 'production' and AUTH_DEV_FALLBACK=true.
Session Rules
- Access JWTs are short-lived and carry
sub = userIdplussid = sessionId. - Refresh tokens are server-stored and revocable; rotation invalidates the prior token, and reuse detection revokes the family.
- Wallet linking is out of scope for v1.
- Product ownership checks use backend records such as
clones.owner_user_id. - Product services should not call MegaETH, Terminal, or wallet providers for app authorization.
Configuration
| Variable | Description | Required |
|---|---|---|
AUTH_JWT_SECRET | HMAC secret for access JWT signing/verification. | Yes |
AUTH_JWT_ISSUER | JWT iss claim. Defaults to pump-party-backend. | No |
AUTH_JWT_AUDIENCE | JWT aud claim. Defaults to pump-party-app. | No |
AUTH_ACCESS_TTL_SECONDS | Access JWT lifetime in seconds. Defaults to 86400 (24 hours). | No |
AUTH_REFRESH_TTL_SECONDS | Refresh token lifetime in seconds. Defaults to 1209600 (14 days). | No |
AUTH_CHALLENGE_TTL_SECONDS | SIWE challenge lifetime in seconds. Defaults to 300 (5 min). | No |
AUTH_ALLOWED_DOMAINS | Comma-separated list of accepted SIWE domain values. Defaults to localhost:3000 in development. | Yes (production) |
AUTH_ALLOWED_CHAIN_IDS | Comma-separated chain IDs accepted by the verifier. Defaults to 4326,6343. | No |
AUTH_DEV_FALLBACK | Set to true to allow the legacy x-user-id header in non-production builds. | No |
MEGAETH_RELAY_URL | Porto Relay endpoint used for non-EOA signature verification. Defaults to https://wallet-relay.megaeth.com. | No |