Skip to content

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-token as 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-122 style 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_wallets open 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 row

The 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. Carries sub = 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, points replaced_by_session_id at 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_id is 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 address

Both proof types should resolve to the same internal result:

text
VerifiedWalletIdentity
  -> address
  -> chainId
  -> walletProvider

Product 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_at

Wallet 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/me

Authenticated 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 = userId plus sid = 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

VariableDescriptionRequired
AUTH_JWT_SECRETHMAC secret for access JWT signing/verification.Yes
AUTH_JWT_ISSUERJWT iss claim. Defaults to pump-party-backend.No
AUTH_JWT_AUDIENCEJWT aud claim. Defaults to pump-party-app.No
AUTH_ACCESS_TTL_SECONDSAccess JWT lifetime in seconds. Defaults to 86400 (24 hours).No
AUTH_REFRESH_TTL_SECONDSRefresh token lifetime in seconds. Defaults to 1209600 (14 days).No
AUTH_CHALLENGE_TTL_SECONDSSIWE challenge lifetime in seconds. Defaults to 300 (5 min).No
AUTH_ALLOWED_DOMAINSComma-separated list of accepted SIWE domain values. Defaults to localhost:3000 in development.Yes (production)
AUTH_ALLOWED_CHAIN_IDSComma-separated chain IDs accepted by the verifier. Defaults to 4326,6343.No
AUTH_DEV_FALLBACKSet to true to allow the legacy x-user-id header in non-production builds.No
MEGAETH_RELAY_URLPorto Relay endpoint used for non-EOA signature verification. Defaults to https://wallet-relay.megaeth.com.No