Lumora is a decentralized fundraising platform built with Next.js, React 19, Zustand, and TanStack React Query. It enables users to create, view, and fund campaigns securely using blockchain-based wallet authentication.
Follow these steps to configure your local development environment.
- Node.js: Version
20.0.0or higher is required (recommendedv20.20.2). - NPM: Version
10.8.2or higher (shipped with Node 20).
Check your installed versions before beginning:
node -v
npm -v-
Clone the repository and navigate to the project directory:
git clone <repository-url> cd lumora-web
-
Standardize your Node.js environment:
nvm use # If Node 20 is not installed, install it: # nvm install 20 && nvm use 20
-
Install project dependencies:
npm install
Copy the example environment file to create your local variables:
cp .env.example .env.localOpen .env.local and define the following variables:
| Variable | Description | Default / Example |
|---|---|---|
NEXT_PUBLIC_API_URL |
The base URL of the backend REST API. | http://localhost:8000 |
NEXT_PUBLIC_ENABLE_DEMO_WALLET |
Enables a development-only mock wallet for testing without extension installs. | true (dev-only) |
Warning
NEXT_PUBLIC_ENABLE_DEMO_WALLET must always be set to false in production. It is strictly gated and will not execute signature fabrications if NODE_ENV=production.
Lumora uses wallet-based authentication following a challenge-response protocol. This guarantees that only the owner of a Stellar address can log in.
sequenceDiagram
autonumber
actor User
participant Frontend as Next.js Client
participant Wallet as Stellar Wallet
participant Backend as API Server
User->>Frontend: Click "Connect Wallet"
Frontend->>Backend: POST /auth/challenge { address }
Backend-->>Frontend: 200 OK { challenge, expiresAt }
Frontend->>Wallet: Request signature for challenge
Wallet-->>Frontend: Return cryptographically signed signature
Frontend->>Backend: POST /auth/verify { address, signature }
Backend-->>Frontend: 200 OK { accessToken, refreshToken, user }
Frontend->>User: Logged in (Zustand state updated)
When integrating a new backend with Lumora, the API must implement the following endpoints and response structures.
Generates a random cryptographic challenge string associated with the public address.
- Endpoint:
POST /auth/challenge - Content-Type:
application/json - Request Payload:
{ "address": "G..." } - Response Payload (200 OK):
{ "challenge": "Sign this random challenge to log in: 9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d", "expiresAt": "2026-08-21T14:05:00.000Z" }
Verifies that the signature is cryptographically valid for the given public address and active challenge.
- Endpoint:
POST /auth/verify - Content-Type:
application/json - Request Payload:
{ "address": "G...", "signature": "base64-encoded-signature-here" } - Response Payload (200 OK):
{ "accessToken": "eyJhbGciOi...", "refreshToken": "eyJhbGciOi...", "user": { "id": "usr-12345", "email": "user@example.com", "name": "Alex Creator", "walletAddress": "G..." } }
Swaps a refresh token for a brand-new access and refresh token pair.
- Endpoint:
POST /auth/refresh - Content-Type:
application/json - Request Payload:
{ "refreshToken": "eyJhbGciOi..." } - Response Payload (200 OK):
{ "accessToken": "eyJhbGciOi...", "refreshToken": "eyJhbGciOi..." }
Instructs the server to blacklist or revoke the active refresh token.
- Endpoint:
POST /auth/logout - Content-Type:
application/json - Headers:
Authorization: Bearer <accessToken> - Response Payload (200 OK):
{ "success": true, "message": "Logged out successfully" }
Starts the Next.js development server with hot-reloading:
npm run devOpen http://localhost:3000 to view the application.
Run ESLint to check for stylistic issues, syntax, or React hooks bugs:
npm run lintTo automatically fix fixable lint problems:
npm run lint:fixRun TypeScript-only checks (no emit):
npm run typecheckWe use Vitest for unit testing:
npm run testRun tests in watch mode during development:
npm run test:watchTo test compilation and optimize the assets:
# 1. Build the production build
npm run build
# 2. Run the production build locally
npm run start- Fabricated Signatures: When
NEXT_PUBLIC_ENABLE_DEMO_WALLETistrue, the frontend skips connecting to the Stellar extension and returns a fixed mock signature. - Production Gating: The demo option is strictly bypassed if
NODE_ENV=production. If you attempt to connect without a wallet in production, the app falls back to a browser-source error notifying the user that no wallet extension is available. - Visual Badges: Active mock sessions are visibly badged as Demo in the navbar to prevent developers from confusing mock environments with mainnet transactions.
- Expiration Gating: Challenges generated via
/auth/challengemust expire in 5 minutes or less (expiresAt). - One-Time Nonces: Nonces must be tracked in the backend database. A nonce must be deleted immediately after it is verified (regardless of success or failure) to prevent replay attacks where an attacker sniffs a signature and tries to re-authenticate.
- Token Rotation (RTR): Upon every token refresh, the old refresh token is immediately blacklisted. If a client attempts to use a blacklisted token, the backend treats this as a theft event and invalidates all session tokens associated with that user.
- Deduplicated Re-Auth: When concurrent API requests fail with 401s, Axios interceptors queue subsequent requests and trigger a single re-auth modal to connect again.
- Cross-Tab Consistency:
providers.tsxchecks for session validity on window focus or storage events. If you log out or disconnect on one tab, other open tabs immediately clear their local storage and redirect to the landing page. - Clear Logout: Logging out triggers token invalidation on the backend and purges cookies, memory state, and localStorage values on the client.
- Server State (TanStack React Query): Controls API calls, query caching, error states, and pagination. Query keys are centralized in
src/lib/queryKeys.tsto ensure cache synchronization. - Client UI State (Zustand): Manages modal visibility, themes, UI filters, and the current active wallet session. Do not duplicate React Query collections into Zustand, as this creates competing sources of truth and stale UI bugs.
Create a local environment file from the example and set the API URL before starting the app:
cp .env.example .env.local
# Edit .env.local and set:
# NEXT_PUBLIC_API_URL=http://localhost:8000
# NEXT_PUBLIC_ENABLE_DEMO_WALLET=true # dev-onlyNEXT_PUBLIC_API_URL: Base URL of the backend REST API (required).NEXT_PUBLIC_ENABLE_DEMO_WALLET: Whentrueenables a development-only demo wallet (never enable in production).
You can run the project with any of the following commands depending on your package manager:
# npm
npm install
npm run dev
# yarn
yarn
yarn dev
# pnpm
pnpm install
pnpm dev
# bun
bun install
bun devnext: 16.2.10react: 19.2.4@tanstack/react-query: ^5.101.2zustand: ^5.0.14axios: ^1.18.1
These are the versions used in package.json and reflected in the TypeScript and ESLint configs.
The frontend expects the following endpoints. These examples show minimal payloads and the expected success responses.
- Request a challenge
curl -X POST "$NEXT_PUBLIC_API_URL/auth/challenge" \
-H "Content-Type: application/json" \
-d '{"address":"G..."}'Success response (200):
{
"challenge": "Sign this random challenge to log in: ...",
"expiresAt": "2026-08-21T14:05:00.000Z"
}- Verify a signature
curl -X POST "$NEXT_PUBLIC_API_URL/auth/verify" \
-H "Content-Type: application/json" \
-d '{"address":"G...","signature":"base64-signature"}'Success response (200):
{
"accessToken": "eyJhbGciOi...",
"refreshToken": "eyJhbGciOi...",
"user": { "id":"usr-123","walletAddress":"G..." }
}- Refresh tokens (rotation)
curl -X POST "$NEXT_PUBLIC_API_URL/auth/refresh" \
-H "Content-Type: application/json" \
-d '{"refreshToken":"eyJ..."}'Success response (200):
{
"accessToken": "eyJhbGciOi...",
"refreshToken": "eyJhbGciOi..."
}- Logout / revoke
curl -X POST "$NEXT_PUBLIC_API_URL/auth/logout" \
-H "Content-Type: application/json" \
-H "Authorization: Bearer <accessToken>"Success response (200):
{ "success": true, "message": "Logged out successfully" }- Start the backend on
http://localhost:8000(or setNEXT_PUBLIC_API_URLaccordingly). - Start the frontend (
npm run dev). - Open the browser, click Connect Wallet and follow the prompt. If using the demo wallet set
NEXT_PUBLIC_ENABLE_DEMO_WALLET=truein.env.local(dev only). - On successful verify the frontend will receive
accessTokenandrefreshToken, persist them in localStorage, and update ZustandauthStore`.
- Never enable the demo wallet in production.
- Backend must enforce challenge expiration (<= 5 minutes) and make nonces one-time use.
- Implement refresh-token rotation (issue new refresh token on every
/auth/refreshand blacklist the old one). - Treat reuse of a rotated refresh token as a theft event and invalidate the session.
-
Demo wallet risks: The demo wallet fabricates signatures and bypasses user-controlled key material. Use it only in local development. Never allow demo signatures against any production backend. Clearly badge demo sessions in the UI and require an opt-in environment flag (
NEXT_PUBLIC_ENABLE_DEMO_WALLET=true) that is ignored whenNODE_ENV=production. -
Nonce (challenge) handling & replay protection:
- Challenges should be single-use values stored server-side and removed after first verification attempt (success or failure).
- Set
expiresAtto a short window (recommended 5 minutes or less) and reject expired challenges. - Log and monitor repeated attempts for the same nonce as a possible replay/attack.
-
Refresh-token rotation:
- On successful
/auth/refreshissue a new refresh token and invalidate the previous one immediately. - Maintain a short rotation window and optionally keep a sequence counter or previous-token reference so the server can detect reuse (the client mirrors this via
applyRefreshRotation). - Treat reuse of an already-rotated token as a theft event: revoke all tokens and require a full re-authentication.
- On successful
-
Concurrent 401 handling:
- The client queues concurrent requests that fail with 401 and allows a single refresh attempt. If refresh fails, queued requests should fail and the user must re-authenticate.
-
Cross-tab & storage safety:
- Use storage events and cookie checks to keep multiple tabs in sync. If a logout is detected in another tab, clear localStorage and reset Zustand state.
- Prefer HttpOnly cookies for access tokens where possible; if using localStorage for tokens, be explicit in the README about XSS risks and audit third-party scripts.
-
Clear logout & token blacklisting:
- On logout call
/auth/logoutto revoke the current refresh token server-side and clear client-side persisted state. - Mark the refresh token as used/blacklisted client-side before purging storage so the backend can detect immediate reuse attempts.
- On logout call
-
Error handling and user feedback:
- Map raw backend and wallet errors to short, actionable messages. Avoid leaking stack traces or detailed error codes in the UI.
POST /auth/challenge: generate and persist a one-time challenge withexpiresAt.POST /auth/verify: validate signature, issueaccessTokenandrefreshToken, and remove the challenge record.POST /auth/refresh: verify refresh token, issue newaccessTokenandrefreshToken, blacklist old refresh token.POST /auth/logout: revoke the provided refresh token and clear server-side sessions.
Follow these rules to interoperate correctly with Lumora's client-side assumptions (token rotation, 401 deduping, and demo-wallet gating).
- API helpers and token rotation:
src/lib/api.ts - Auth and token storage:
src/stores/authStore.ts - Re-auth modal and cross-tab logic:
src/app/providers.tsxandsrc/components/ReAuthModal.tsx
If you want, I can now add explicit curl examples to src/lib/api.ts comments or update src/app/layout.tsx metadata to include the runtime versions. Which would you prefer next?