# QuickDapp Documentation Version: 3.11.3 This is the complete QuickDapp documentation in a plain text format optimized for LLM context windows. --- # Chain Monitoring The Web3 variant includes a blockchain event monitoring system that polls for on-chain events and processes them into user notifications. This is how the app stays in sync with contract activity without requiring users to refresh. ## How It Works The chain monitoring system has three layers: 1. **`watchChain` worker job** — A cron-scheduled background job that polls the blockchain for new events 2. **Chain filter modules** — Event handlers that define which events to watch and how to process them 3. **Notification delivery** — Processed events become user notifications delivered via the existing notification system ``` Blockchain → watchChain job → Chain Filters → Notifications → WebSocket → User ``` ## watchChain Worker Job The [`watchChain`](https://github.com/QuickDapp/QuickDapp/blob/v3.11.3/src/server/workers/jobs/watchChain.ts) job runs on a cron schedule (every 3 seconds by default). Each execution: 1. Gets the current block number from the chain 2. Loads the last processed block from the database (`settings` table) 3. Calculates the block range to process (capped at 500 blocks per run to avoid timeout) 4. Runs each chain filter module against the block range 5. Updates the last processed block in the database On first run, the watcher starts from the current block (in production) or block 1 (in test mode). This means it only processes events that occur after deployment — historical events are not backfilled. ### Block Range Limiting To prevent long-running queries against the RPC endpoint, each run processes at most 500 blocks. If the app falls behind (e.g., after being offline), it catches up incrementally over multiple runs. ### Persistence The last processed block is stored per chain in the database. This means the watcher survives server restarts without reprocessing blocks or missing events. ## Chain Filter Modules Chain filters are modular event handlers registered in the `watchChain` job. Each filter module implements the `ChainLogModule` interface: ```typescript interface ChainLogModule { getEvent: () => AbiEvent getContractAddress: () => `0x${string}` | null processLogs: (serverApp: ServerApp, log: Logger, logs: Log[]) => Promise } ``` - `getEvent()` — Returns the ABI event definition to watch for - `getContractAddress()` — Returns a specific contract address to filter by, or `null` to watch all contracts - `processLogs()` — Handles matched events (e.g., creating notifications) ### Built-in Filters The Web3 variant includes two chain filter modules: #### createToken [`src/server/workers/chainFilters/createToken.ts`](https://github.com/QuickDapp/QuickDapp/blob/v3.11.3/src/server/workers/chainFilters/createToken.ts) watches for `ERC20NewToken` events from the factory contract: ```solidity event ERC20NewToken( address indexed token, string name, string symbol, address indexed creator, uint256 initialSupply ) ``` When a new token is created, the filter: 1. Extracts the creator address from the event 2. Looks up the user by wallet address in the `userAuth` table 3. Creates a `TOKEN_CREATED` notification for that user In production, it only watches the configured factory contract address. In test mode, it watches all contracts to support test factories. #### sendToken [`src/server/workers/chainFilters/sendToken.ts`](https://github.com/QuickDapp/QuickDapp/blob/v3.11.3/src/server/workers/chainFilters/sendToken.ts) watches for `TokenTransferred` events from SimpleERC20 contracts: ```solidity event TokenTransferred( address indexed from, address indexed to, uint256 value, string name, string symbol, uint8 decimals ) ``` When a token transfer occurs, the filter: 1. Extracts the sender address from the event 2. Looks up the user by wallet address 3. Creates a `TOKEN_TRANSFER` notification with the formatted transfer amount This filter watches all contract addresses since any SimpleERC20 token can emit these events. ### Adding Custom Filters To add a new chain filter: 1. Create a module in `src/server/workers/chainFilters/`: ```typescript import type { AbiEvent } from "viem" import { parseAbiItem } from "viem" import type { ChainLogModule } from "../jobs/types" const MY_EVENT = parseAbiItem( "event MyEvent(address indexed sender, uint256 value)" ) export const getEvent: ChainLogModule["getEvent"] = () => { return MY_EVENT as AbiEvent } export const getContractAddress: ChainLogModule["getContractAddress"] = () => { return "0x1234..." as `0x${string}` // or null to watch all contracts } export const processLogs: ChainLogModule["processLogs"] = async ( serverApp, log, logs ) => { for (const logEntry of logs) { const { args: { sender, value } } = logEntry // Process the event... } } ``` 2. Register it in `watchChain.ts`: ```typescript import * as myFilter from "../chainFilters/myFilter" const chainLogModules: ChainLogRegistry = { sendToken: sendTokenFilter, createToken: createTokenFilter, myFilter: myFilter, // Add here } ``` ## deployMulticall3 Job The [`deployMulticall3`](https://github.com/QuickDapp/QuickDapp/blob/v3.11.3/src/server/workers/jobs/deployMulticall3.ts) worker job ensures the Multicall3 contract is available on the development chain. It runs once on startup: 1. Checks if Multicall3 is already deployed at the expected address 2. If not deployed, funds the deterministic deployer address 3. Sends the pre-signed deployment transaction 4. Verifies the contract was deployed successfully This is only necessary on local development chains (Anvil) where Multicall3 isn't available by default. On mainnet, testnets, and L2s, Multicall3 is already deployed at a well-known address. ## Notification Flow When a chain filter detects a relevant event, it creates a notification through `serverApp.createNotification()`. This: 1. Inserts the notification into the `notifications` database table 2. Sends a real-time WebSocket message to the user's connected browser sessions 3. The frontend `NotificationsIndicator` updates the unread badge count 4. The `NotificationsDialog` shows the new notification with event-specific details This means users see blockchain activity in real time without polling — the chain watcher polls the blockchain, and WebSockets push updates to the browser. --- # Smart Contracts The Web3 variant includes sample smart contracts demonstrating an ERC20 token factory pattern. These serve as a starting point for your own contract development and show how QuickDapp integrates with on-chain contracts end-to-end. ## Overview The sample contracts use OpenZeppelin contracts and Foundry tooling. The factory deploys new ERC20 tokens with custom names, symbols, and initial supplies. Both contracts live in [`sample-contracts/src/ERC20Factory.sol`](https://github.com/QuickDapp/QuickDapp/blob/v3.11.3/sample-contracts/src/ERC20Factory.sol): - **`ERC20Factory`** - Deploys new ERC20 tokens and tracks them - **`SimpleERC20`** - Basic ERC20 with mint/burn capabilities and custom transfer events ## Directory Structure ``` sample-contracts/ ├── src/ │ └── ERC20Factory.sol # Factory and token contracts ├── devnet.ts # Local Hardhat blockchain ├── deploy.ts # Foundry deployment script ├── foundry.toml # Foundry configuration └── hardhat.config.cjs # Hardhat node configuration ``` ## Local Development Start the local blockchain and deploy contracts in separate terminals: ```shell # Terminal 1: Start local blockchain cd sample-contracts bun devnet.ts # Terminal 2: Deploy contracts cd sample-contracts bun deploy.ts ``` The deployment script uses Foundry's `forge create` to deploy `ERC20Factory`, then automatically updates `.env.local` with the deployed contract address. ## Factory Interface The factory tracks deployed tokens and emits events for indexing: ```solidity contract ERC20Factory { event ERC20NewToken( address indexed token, string name, string symbol, address indexed creator, uint256 initialSupply ); function erc20DeployToken( ERC20TokenConfig memory config, uint256 initialBalance ) external returns (address); function getNumErc20s() external view returns (uint256); function getErc20Address(uint256 index) external view returns (address); function getAllErc20Addresses() external view returns (address[] memory); } ``` Token configuration uses a struct: ```solidity struct ERC20TokenConfig { string name; string symbol; uint8 decimals; } ``` ## Testnet Deployment Deploy to Sepolia or other testnets by configuring environment variables: ```shell # In your .env.local WEB3_SUPPORTED_CHAINS=sepolia WEB3_SEPOLIA_RPC=https://sepolia.infura.io/v3/YOUR_KEY WEB3_SERVER_WALLET_PRIVATE_KEY=0x... # Deploy cd sample-contracts bun deploy.ts ``` The script reads from the first chain in `WEB3_SUPPORTED_CHAINS` and uses the corresponding RPC endpoint. ## Testing Run Foundry tests for contract verification: ```shell cd sample-contracts forge test # Run all tests forge test -vvv # Verbose output forge test --gas-report ``` ## Frontend Integration After deployment, the contract address is available via [`clientConfig.WEB3_FACTORY_CONTRACT_ADDRESS`](https://github.com/QuickDapp/QuickDapp/blob/v3.11.3/src/shared/config/client.ts). ABIs are generated from Foundry build artifacts to [`src/shared/abi/generated.ts`](https://github.com/QuickDapp/QuickDapp/blob/v3.11.3/src/shared/abi/generated.ts) when you run `bun run gen`. Use Wagmi hooks on the client for contract interactions. The `ERC20NewToken` and `TokenTransferred` events are tracked by the [chain monitoring](chain-monitoring) worker for real-time notifications. ## Customization To add your own contracts: 1. Add Solidity files to `sample-contracts/src/` 2. Run `forge build` to compile 3. Update [`deploy.ts`](https://github.com/QuickDapp/QuickDapp/blob/v3.11.3/sample-contracts/deploy.ts) to deploy your contracts 4. Run `bun run gen` to regenerate ABI types --- # Authentication The Web3 variant replaces the base package's email/OAuth-first authentication with **SIWE (Sign-In With Ethereum)** — users authenticate by signing a message with their wallet. Email and OAuth authentication remain available as secondary options. ## How SIWE Works SIWE is a standard for using Ethereum wallets to authenticate with web applications. Instead of passwords or OAuth tokens, the user proves ownership of their wallet address by signing a structured message. The authentication flow: 1. **User connects wallet** — RainbowKit prompts the user to select and connect a wallet 2. **Client requests SIWE message** — The `generateSiweMessage` GraphQL mutation creates a message containing the domain, address, chain ID, and a random nonce 3. **Server generates message** — The server creates a SIWE-formatted message with the nonce stored for verification 4. **User signs message** — The wallet prompts the user to sign the message (no gas cost) 5. **Client sends signature** — The signed message is sent to `authenticateWithSiwe` 6. **Server verifies** — The server verifies the signature matches the address, checks the domain and nonce, then creates or retrieves the user account 7. **JWT issued** — A JWT token is returned containing `userId` and `web3_wallet` fields ```graphql # Step 1: Generate SIWE message mutation { generateSiweMessage( address: "0x1234..." chainId: 1 domain: "localhost:3000" ) { message nonce } } # Step 2: Authenticate with signed message mutation { authenticateWithSiwe( message: "localhost:3000 wants you to sign in..." signature: "0xabcd..." ) { success token web3Wallet error } } ``` ## Domain Validation The server validates that the SIWE message domain matches an allowed origin. Configure allowed origins with `WEB3_ALLOWED_SIWE_ORIGINS`: ```shell # .env.local WEB3_ALLOWED_SIWE_ORIGINS=http://localhost:3000,https://myapp.com ``` In production, this prevents phishing attacks where a malicious site could trick users into signing messages for your domain. ## Frontend AuthContext The Web3 variant's `AuthContext` is a state machine built on `useReducer` that coordinates wallet connection with SIWE authentication. ### State Machine ``` IDLE ──────────────────► AUTHENTICATING ──► AUTHENTICATED │ │ │ │ ├──► REJECTED │ │ │ │ │ └──► ERROR │ │ │ └── (on mount) ──► RESTORING ──► WAITING_FOR_WALLET ──► AUTHENTICATED │ └──► IDLE (no saved session) ``` **Key transitions:** - On mount, the context checks localStorage for a saved token. If found, it validates with the server and enters `WAITING_FOR_WALLET` until the wallet reconnects - When a wallet connects and the user is in `IDLE`, authentication starts automatically (unless the user previously rejected signing for that address) - If the user rejects the signature prompt, the state moves to `REJECTED` and auto-authentication is suppressed for that address until a different wallet connects - Wallet disconnection while authenticated triggers an automatic logout ### Interface ```typescript interface AuthContextValue { isAuthenticated: boolean isLoading: boolean error: Error | null authToken: string | null walletAddress: string | null userRejectedAuth: boolean authenticate: (address: string) => Promise logout: () => void restoreAuth: () => void } ``` The `walletAddress` and `userRejectedAuth` properties are specific to the Web3 variant. The `authenticate` method takes a wallet address and runs the full SIWE flow. ### Auto-Authentication When a wallet connects and the user hasn't rejected signing for that address, the context automatically calls `authenticate(address)` after a short delay to ensure the wallet connector is fully ready. This provides a seamless experience — connecting a wallet immediately triggers sign-in. If the user rejects the signature, the address is tracked so auto-authentication won't trigger again until a different wallet is connected or the page is refreshed. ## JWT Token The JWT token includes a `web3_wallet` field alongside the standard `userId`: ```json { "type": "auth", "userId": 42, "web3_wallet": "0x1234...abcd", "iat": 1700000000, "exp": 1700086400, "jti": "unique-token-id" } ``` The `validateToken` query also returns the `web3Wallet` field, which the frontend uses during session restoration to verify the saved wallet matches. ## ConnectWallet Component The [`ConnectWallet`](https://github.com/QuickDapp/QuickDapp/blob/v3.11.3/src/client/components/ConnectWallet.tsx) component uses RainbowKit's `ConnectButton.Custom` API to render wallet connection UI. It shows different states based on the connection and authentication status: - **Disconnected** — A "Connect Wallet" button - **Wrong network** — A warning with a chain switcher - **Connected** — The wallet address and account details - **Optional `showNetwork` prop** — Adds a chain selector button alongside the account The component coordinates with `AuthContext` — connecting a wallet triggers SIWE authentication automatically. ## Account Linking Users who authenticate with SIWE can also link email and OAuth accounts. The web3 wallet address is stored as an auth method in the `userAuth` table alongside any other linked methods. This means a user who initially signed in with their wallet can later add email authentication to the same account. When authenticating with email or OAuth, the server checks if the user already has a web3 wallet linked and includes it in the JWT token, maintaining wallet context across auth methods. --- # Blockchain Interactions The Web3 variant provides a layered system for reading from and writing to smart contracts, with multicall batching for efficiency and retry logic for reliability. ## Chain Configuration Chain support is defined in [`src/shared/contracts/chain.ts`](https://github.com/QuickDapp/QuickDapp/blob/v3.11.3/src/shared/contracts/chain.ts). The `WEB3_SUPPORTED_CHAINS` environment variable controls which chains are available: ```shell WEB3_SUPPORTED_CHAINS=anvil,sepolia,mainnet ``` The first chain in the list is the **primary chain** — the one used for server-side operations (blockchain clients, chain watching, contract deployment). Available chains: | Name | Chain ID | Description | |------|----------|-------------| | `anvil` | 31337 | Local Hardhat/Anvil development chain | | `mainnet` | 1 | Ethereum mainnet | | `sepolia` | 11155111 | Sepolia testnet | | `base` | 8453 | Base L2 | Each chain can have a custom RPC endpoint via environment variables (`WEB3_ANVIL_RPC`, `WEB3_MAINNET_RPC`, etc.). If no custom RPC is specified, Viem's built-in public RPCs are used. ### Utility Functions ```typescript import { getChain, getChainId, getSupportedChains, getPrimaryChain } from "@shared/contracts/chain" getSupportedChains() // All configured chains as viem Chain objects getPrimaryChain() // First chain in WEB3_SUPPORTED_CHAINS getChain("sepolia") // Get a specific chain by name getChainId("mainnet") // Get chain ID by name ``` ## Server-Side Blockchain Access The `ServerApp` type includes two blockchain clients created during bootstrap: ```typescript // publicClient — read-only access to chain state const blockNumber = await serverApp.publicClient.getBlockNumber() const balance = await serverApp.publicClient.getBalance({ address }) // walletClient — authenticated with WEB3_SERVER_WALLET_PRIVATE_KEY const hash = await serverApp.walletClient.sendTransaction({ to, value }) ``` The `publicClient` connects to the primary chain's RPC endpoint. The `walletClient` uses `privateKeyToAccount()` with the configured server wallet key, allowing the server to send transactions (e.g., deploying contracts, executing administrative operations). ## Contract Reader The contract reader in [`src/shared/contracts/reader.ts`](https://github.com/QuickDapp/QuickDapp/blob/v3.11.3/src/shared/contracts/reader.ts) provides typed contract read operations. ### Single Read ```typescript import { readContract } from "@shared/contracts/reader" const totalTokens = await readContract( { address: factoryAddress, abi: factoryAbi, functionName: "getNumErc20s", }, publicClient ) ``` ### Batched Reads (Multicall) For multiple reads, `readContractMultiple` uses Multicall3 to batch them into a single RPC call: ```typescript import { readContractMultiple } from "@shared/contracts/reader" const [name, symbol, decimals] = await readContractMultiple( [ { address: tokenAddr, abi: erc20Abi, functionName: "name" }, { address: tokenAddr, abi: erc20Abi, functionName: "symbol" }, { address: tokenAddr, abi: erc20Abi, functionName: "decimals" }, ], publicClient ) ``` ### Fault-Tolerant Reads `readContractMultipleWithFallback` returns partial results when some calls fail: ```typescript import { readContractMultipleWithFallback } from "@shared/contracts/reader" const { results, errors, successCount } = await readContractMultipleWithFallback( calls, publicClient ) // results: (T | null)[] — null for failed calls ``` ### Retry Logic `readContractWithRetry` adds exponential backoff for unreliable RPC endpoints: ```typescript import { readContractWithRetry } from "@shared/contracts/reader" const result = await readContractWithRetry( call, publicClient, 3, // max retries 1000 // base delay in ms (doubles each retry) ) ``` ## Contract Writer The contract writer in [`src/shared/contracts/writer.ts`](https://github.com/QuickDapp/QuickDapp/blob/v3.11.3/src/shared/contracts/writer.ts) handles transaction execution with simulation, state tracking, and callbacks. ### ContractWriterInstance The `ContractWriterInstance` class manages the full lifecycle of a contract write: 1. **Simulate** — Dry-run the transaction to catch errors before spending gas 2. **Submit** — Send the transaction to the blockchain 3. **Confirm** — Wait for the transaction to be included in a block 4. **Verify** — Check the transaction receipt status ```typescript import { createContractWriter, createContractWrite } from "@shared/contracts/writer" const request = createContractWrite( factoryAddress, factoryAbi, "erc20DeployToken", [tokenConfig, initialBalance] ) const writer = createContractWriter(publicClient, walletClient, request) const receipt = await writer.exec({ onTransactionSubmitted: (txHash) => { console.log("Submitted:", txHash) }, onTransactionConfirmed: (receipt) => { console.log("Confirmed in block:", receipt.blockNumber) }, }) ``` ### State Tracking The writer tracks its state throughout execution: ```typescript interface ContractWriterState { isLoading: boolean // Transaction in progress isSuccess: boolean // Transaction confirmed successfully error: Error | null // Error if any step failed txHash?: `0x${string}` // Transaction hash (available after submission) receipt?: TransactionReceipt // Receipt (available after confirmation) } ``` Call `writer.reset()` to clear state and allow retries. ### Quick Write For one-off writes without state tracking, use the `writeContract` function: ```typescript import { writeContract } from "@shared/contracts/writer" const receipt = await writeContract(publicClient, walletClient, request) ``` ## Multicall3 [Multicall3](https://www.multicall3.com/) batches multiple contract reads into a single RPC call, reducing latency and rate limit consumption. The multicall implementation in [`src/shared/contracts/multicall.ts`](https://github.com/QuickDapp/QuickDapp/blob/v3.11.3/src/shared/contracts/multicall.ts) uses the `aggregate3` function, which allows individual calls to fail without breaking the entire batch. If Multicall3 is unavailable or fails, the system automatically falls back to individual contract calls. On local development chains (Anvil), the [`deployMulticall3`](chain-monitoring) worker job automatically deploys Multicall3 on startup using a deterministic deployment transaction, so multicall works identically in development and production. ## ABI Generation Contract ABIs are generated from Foundry build artifacts during `bun run gen`. The codegen process: 1. Reads compiled contract artifacts from `sample-contracts/out/` 2. Extracts the ABI JSON for each contract 3. Generates TypeScript types in `src/shared/abi/generated.ts` This ensures type safety between your Solidity contracts and TypeScript code — contract function names, argument types, and return types are all statically checked. --- # Frontend The Web3 variant adds blockchain-specific frontend components, hooks, and configuration on top of the base package's React setup. ## Configuration The [`createWeb3Config()`](https://github.com/QuickDapp/QuickDapp/blob/v3.11.3/src/client/config/web3.ts) function sets up RainbowKit with the supported chains and WalletConnect project ID. It uses RainbowKit's `getDefaultConfig()` for a simple setup with standard wallet options. Chain configuration lives in [`src/shared/contracts/chain.ts`](https://github.com/QuickDapp/QuickDapp/blob/v3.11.3/src/shared/contracts/chain.ts). The `WEB3_SUPPORTED_CHAINS` environment variable specifies which networks to enable. Supported chains include `anvil` (local development), `mainnet`, `sepolia`, and `base`. The first chain in the list becomes the primary chain. ## Provider Structure The Web3 variant wraps the base provider stack with `WagmiProvider` and `RainbowKitProvider`: ```tsx {/* App content */} ``` RainbowKit's theme automatically adapts to the app's light/dark mode via the `useTheme()` hook. ## Wallet Connection The [`ConnectWallet`](https://github.com/QuickDapp/QuickDapp/blob/v3.11.3/src/client/components/ConnectWallet.tsx) component uses RainbowKit's `ConnectButton.Custom` API to render wallet connection UI. It coordinates with [`AuthContext`](https://github.com/QuickDapp/QuickDapp/blob/v3.11.3/src/client/contexts/AuthContext.tsx) to trigger SIWE authentication after wallet connection. The component shows different states: a connect button when disconnected, a "Wrong network" warning for unsupported chains, and the connected account address when authenticated. The optional `showNetwork` prop adds a chain selector button. ## Token Hooks Two hooks handle ERC-20 token operations: [`useTokens.ts`](https://github.com/QuickDapp/QuickDapp/blob/v3.11.3/src/client/hooks/useTokens.ts) provides read operations: - `useMyTokens()` — Fetches all tokens where the connected wallet has a balance, using multicall for efficiency - `useTokenInfo(address)` — Fetches details for a specific token (name, symbol, decimals, balance) - `useTokenCount()` — Returns the total number of tokens deployed through the factory [`useTokenActions.ts`](https://github.com/QuickDapp/QuickDapp/blob/v3.11.3/src/client/hooks/useTokenActions.ts) provides write operations: - `useCreateToken()` — Deploys a new ERC-20 token through the factory contract - `useTransferToken()` — Transfers tokens to another address - `useTransactionStatus(hash)` — Tracks transaction confirmation status These hooks use React Query for caching and automatic refetching. Token queries refresh every 5-30 seconds to keep balances current. ## Contract Utilities The [`src/shared/contracts/`](https://github.com/QuickDapp/QuickDapp/blob/v3.11.3/src/shared/contracts/) folder provides utilities for contract interactions: - `getFactoryContractInfo()` — Returns the factory contract address and ABI - `getERC20ContractInfo(address)` — Returns ERC-20 ABI for any token address - `readContract()` — Reads contract state with proper typing - `writeContract()` — Sends transactions through the wallet - `fetchTokenWithBalance()` — Fetches token metadata and balance in one multicall Contract ABIs are generated from Solidity files during build. See [`src/shared/abi/generated.ts`](https://github.com/QuickDapp/QuickDapp/blob/v3.11.3/src/shared/abi/generated.ts) for the generated types. ## Token Components Several components handle token display and management: - [`TokenList`](https://github.com/QuickDapp/QuickDapp/blob/v3.11.3/src/client/components/TokenList.tsx) — Displays all tokens owned by the connected wallet - [`CreateTokenDialog`](https://github.com/QuickDapp/QuickDapp/blob/v3.11.3/src/client/components/CreateTokenDialog.tsx) — Modal form for deploying new tokens - [`SendTokenDialog`](https://github.com/QuickDapp/QuickDapp/blob/v3.11.3/src/client/components/SendTokenDialog.tsx) — Modal form for transferring tokens - [`ContractValue`](https://github.com/QuickDapp/QuickDapp/blob/v3.11.3/src/client/components/ContractValue.tsx) — Displays values read from contracts - [`NumTokens`](https://github.com/QuickDapp/QuickDapp/blob/v3.11.3/src/client/components/NumTokens.tsx) — Shows the total token count from the factory - [`IfWalletConnected`](https://github.com/QuickDapp/QuickDapp/blob/v3.11.3/src/client/components/IfWalletConnected.tsx) — Conditionally renders children only when a wallet is connected ## Multicall Token operations use Viem's multicall support to batch multiple contract reads into a single RPC request. The [`fetchMultipleTokensWithBalances()`](https://github.com/QuickDapp/QuickDapp/blob/v3.11.3/src/shared/contracts/tokens.ts) function demonstrates this — it fetches name, symbol, decimals, and balance for multiple tokens in one call. For development on Anvil, QuickDapp automatically deploys Multicall3 if it doesn't exist. See [Chain Monitoring](chain-monitoring) for details on the `deployMulticall3` worker job. ## Error Handling Transaction errors are caught and displayed through the toast system. Common errors include: - User rejected the transaction - Insufficient funds for gas - Contract reverted (with the revert reason if available) The hooks return `error` and `isError` states so components can display appropriate feedback. --- # Web3 The Web3 variant extends the base QuickDapp package with comprehensive blockchain integration. It adds wallet-based authentication, smart contract deployment and interaction, on-chain event monitoring, and token management — everything you need to build a full-featured decentralized application. ## What It Adds On top of the base package, the Web3 variant introduces: - **Wallet authentication** via [SIWE](https://login.xyz/) (Sign-In With Ethereum) — users authenticate by signing a message with their wallet - **Smart contract tooling** — sample ERC20 factory contracts, Foundry for compilation/testing, Hardhat for local blockchain - **Blockchain clients** on `ServerApp` — `publicClient` for reading chain state, `walletClient` for server-side transactions - **Chain event monitoring** — a background worker that polls for on-chain events and creates user notifications - **Token management** — hooks and components for creating, transferring, and displaying ERC20 tokens - **Multicall3 support** — automatic deployment and batched contract reads for efficiency ## Technologies Added | Technology | Purpose | |-----------|---------| | [RainbowKit](https://www.rainbowkit.com/) | Wallet connection UI with multiple wallet support | | [Wagmi](https://wagmi.sh/) | React hooks for Ethereum interactions | | [Viem](https://viem.sh/) | TypeScript Ethereum client library | | [SIWE](https://login.xyz/) | Sign-In With Ethereum standard | | [Hardhat](https://hardhat.org/) | Local Ethereum blockchain for development | | [Foundry](https://book.getfoundry.sh/) | Smart contract compilation, testing, and deployment | ## Architecture Differences The Web3 variant modifies the base architecture in several key areas: ### ServerApp The `ServerApp` type gains two blockchain client properties: ```typescript type ServerApp = { // ... base properties ... publicClient: PublicClient // Read chain state walletClient: WalletClient // Send transactions } ``` These are created during bootstrap using the configured chain and RPC endpoint, authenticated with `WEB3_SERVER_WALLET_PRIVATE_KEY`. ### Provider Stack The frontend wraps the base provider stack with Web3-specific providers: ``` ThemeProvider WagmiProvider QueryClientProvider RainbowKitProvider AuthProvider (SIWE-based) SocketProvider ToastProvider ``` `WagmiProvider` manages wallet connections and chain state. `RainbowKitProvider` provides the wallet connection UI and adapts its theme to match the app's light/dark mode. ### Authentication The `AuthContext` uses a state machine with 7 statuses instead of the base package's 5: | Status | Description | |--------|-------------| | `IDLE` | No authentication attempt | | `RESTORING` | Checking for saved session | | `WAITING_FOR_WALLET` | Token validated, waiting for wallet reconnection | | `AUTHENTICATING` | SIWE signing in progress | | `AUTHENTICATED` | Signed in with valid token | | `REJECTED` | User rejected the signature request | | `ERROR` | Authentication failed | ### Worker Jobs Two additional built-in worker jobs: - `watchChain` — Polls the blockchain for events and processes them through chain filter modules - `deployMulticall3` — Deploys the Multicall3 contract on local development chains (Anvil) if not already present ## Configuration The Web3 variant requires additional environment variables beyond the base package: | Variable | Required | Description | |----------|----------|-------------| | `WEB3_WALLETCONNECT_PROJECT_ID` | Yes | WalletConnect Cloud project ID | | `WEB3_SUPPORTED_CHAINS` | Yes | Comma-separated chain names (e.g., `anvil,sepolia`) | | `WEB3_SERVER_WALLET_PRIVATE_KEY` | Yes | Server wallet private key for transactions | | `WEB3_ALLOWED_SIWE_ORIGINS` | Yes | Allowed origins for SIWE domain validation | | `WEB3_FACTORY_CONTRACT_ADDRESS` | Yes | Deployed ERC20Factory contract address | | `WEB3_ANVIL_RPC` | No | Custom RPC URL for local Anvil chain | | `WEB3_MAINNET_RPC` | No | Custom RPC URL for Ethereum mainnet | | `WEB3_SEPOLIA_RPC` | No | Custom RPC URL for Sepolia testnet | | `WEB3_BASE_RPC` | No | Custom RPC URL for Base chain | See [Environment Variables](/docs/3.11.3/environment-variables) for the full list including base package variables. --- # Variants QuickDapp variants are standalone derivations of the base package, each tailored for a specific domain or use case. They share the same core architecture — ElysiaJS, PostgreSQL, GraphQL, React — but add specialized features on top. ## How Variants Work Each variant is a complete, self-contained project. It includes everything from the base package plus its domain-specific additions: extra database tables, new GraphQL operations, additional frontend components, and specialized worker jobs. You clone a variant and develop it as your own project, just like the base package. Variants are not plugins or add-ons. They don't depend on the base package at runtime. Instead, they started as copies of base and evolved with their specific features. This means you get a single, cohesive codebase with no indirection or abstraction layers between base and variant functionality. ## Available Variants | Variant | Description | |---------|-------------| | [Web3](web3/) | Blockchain integration with wallet authentication (SIWE), smart contract interactions, token management, and chain event monitoring | ## Building Your Own Variant QuickDapp is designed as a framework for building specialized starter kits. To create your own variant: 1. Start with the base package (or an existing variant close to your needs) 2. Add your domain-specific features — database tables, GraphQL operations, frontend components, worker jobs 3. Include any additional dependencies and configuration 4. Add documentation for your variant's features The base package provides the foundation: authentication, user management, background jobs, real-time notifications, and a complete dev/build/deploy toolchain. Your variant adds everything specific to your domain. ## Community Variants Community-contributed variants are on the roadmap. If you build a variant that others might find useful, it could be published as an official QuickDapp variant in the future. --- # E2E Testing QuickDapp includes end-to-end browser testing powered by [Playwright](https://playwright.dev/). E2E tests verify the full application stack from the user's perspective — clicking buttons, filling forms, and navigating pages. ## How the E2E Runner Works The E2E test runner (`scripts/test-e2e.ts`) handles all the setup: 1. **Starts the test database** — Runs `docker compose -f docker-compose.test.yaml up` to start a PostgreSQL container (skipped in CI where the database is provided as a service) 2. **Pushes the schema** — Runs `bun run db push --force` to set up the database schema 3. **Starts the dev server** — Playwright's `webServer` config automatically starts the Vite dev server and waits for it 4. **Runs Playwright tests** — Executes tests in headless Chromium by default 5. **Reports results** — Outputs pass/fail status with detailed error information ## Running E2E Tests ```shell bun run test:e2e # Run headless browser tests bun run test:e2e --headed # Run with visible browser window bun run test:e2e --ui # Open Playwright's interactive UI mode ``` ## Configuration The Playwright configuration lives in `playwright.config.ts`: | Setting | Value | |---------|-------| | Test directory | `./tests/e2e` | | Base URL | `http://localhost:5173` | | Browser | Chromium (Desktop Chrome) | | Parallel | Disabled (tests run sequentially) | | Web server | Automatically starts dev server | Tests run sequentially to avoid complications with shared server state and database. For faster parallel execution, consider the integration test suite instead. ## Writing E2E Tests E2E tests go in the `tests/e2e/` directory: ```typescript import { test, expect } from "@playwright/test" test("homepage loads", async ({ page }) => { await page.goto("/") await expect(page).toHaveTitle(/QuickDapp/) }) test("can navigate to login", async ({ page }) => { await page.goto("/") await page.click("text=Sign In") await expect(page.locator("form")).toBeVisible() }) ``` Playwright provides powerful selectors and assertions. See the [Playwright documentation](https://playwright.dev/docs/writing-tests) for details. ## CI vs Local The test runner behaves differently based on environment: | Behavior | Local | CI (`CI=true`) | |----------|-------|----------------| | Database | Docker Compose starts container | Service container provided | | Retries | None (fail fast) | Up to 2 retries | | `.only` | Allowed | Blocked (`--forbidOnly`) | In CI, tests retry on failure to handle flaky browser interactions. Locally, failures stop immediately for faster debugging. ## Common Patterns ### Authenticated Tests For tests that require login: ```typescript import { test, expect } from "@playwright/test" test("authenticated user can access dashboard", async ({ page }) => { // Navigate to login await page.goto("/login") // Fill login form await page.fill('input[name="email"]', "test@example.com") await page.fill('input[name="password"]', "password123") await page.click('button[type="submit"]') // Wait for redirect and verify dashboard await expect(page).toHaveURL(/dashboard/) await expect(page.locator("h1")).toContainText("Dashboard") }) ``` ### Form Interactions Testing form submission: ```typescript test("can submit contact form", async ({ page }) => { await page.goto("/contact") await page.fill('input[name="name"]', "Test User") await page.fill('input[name="email"]', "test@example.com") await page.fill('textarea[name="message"]', "Hello!") await page.click('button[type="submit"]') await expect(page.locator(".success-message")).toBeVisible() }) ``` ### Waiting for Network When tests depend on API responses: ```typescript test("loads user data", async ({ page }) => { await page.goto("/profile") // Wait for the GraphQL response await page.waitForResponse( (response) => response.url().includes("/graphql") && response.status() === 200 ) await expect(page.locator(".user-name")).toBeVisible() }) ``` ## Debugging Use Playwright's UI mode for interactive debugging: ```shell bun run test:e2e --ui ``` This opens a visual interface where you can: - Step through tests one action at a time - See page snapshots at each step - Inspect the DOM state - View console logs and network requests For headed mode with slower execution: ```shell bun run test:e2e --headed ``` See [`playwright.config.ts`](https://github.com/QuickDapp/QuickDapp/blob/v3.11.3/playwright.config.ts) for the full configuration. --- # Testing QuickDapp includes a robust testing infrastructure built on Bun's native test runner. The system supports parallel test execution with complete database isolation, ensuring tests run quickly without interfering with each other. ## Architecture Overview The test framework consists of three main parts: - **Test runner** (`scripts/test.ts`) — Orchestrates parallel test execution, manages database templates, and tracks test durations - **Template database** — A pre-configured PostgreSQL database cloned for each test file, avoiding schema setup overhead - **Test helpers** (`tests/helpers/`) — Utilities for starting test servers, managing authentication, and configuring isolated resources ## How Parallel Execution Works Each test file receives its own isolated environment: | Resource | Allocation | |----------|-----------| | Server port | 54000 + file index | | Database | `quickdapp_test_{index}` cloned from template | | ServerApp | Fresh instance per test file | Before tests run, the runner pushes the schema to a template database (`quickdapp_test`). Each test file then clones this template, runs its tests, and drops the cloned database afterward. This approach provides complete isolation while avoiding the cost of running migrations for every file. ## Duration-Based Ordering The file `tests/test-run-order.json` tracks how long each test file takes to execute. The test runner orders files by duration (longest first) to optimize parallel execution — starting slow tests early ensures they don't become bottlenecks at the end. This file is automatically updated after each test run. Commit it to share timing data across the team. ## Test Directory Structure ``` tests/ ├── helpers/ │ ├── test-config.ts # Port and database allocation │ ├── server.ts # Test server lifecycle │ ├── auth.ts # Authentication helpers │ └── logger.ts # Test logger ├── server/ │ ├── auth/ # Authentication tests │ └── graphql/ # GraphQL API tests ├── setup.ts # Global test setup └── test-run-order.json # Duration tracking ``` ## Critical Requirement: Import Order All test files **must** import `@tests/helpers/test-config` as their first import: ```typescript import "@tests/helpers/test-config" // Must be first! import { beforeAll, afterAll, test, expect } from 'bun:test' import { startTestServer } from '../helpers/server' ``` This import sets `PORT`, `DATABASE_URL`, and `API_URL` environment variables before `serverConfig` caches them at module load time. Importing it after other server modules causes tests to use the wrong port or database. ## Running Tests ```shell bun run test # Run all tests bun run test --pattern auth # Run matching tests bun run test --watch # Watch mode bun run test --verbose # Enable debug logging bun run test --bail # Stop on first failure bun run test -c 4 # Set concurrency level bun run test -f auth.test.ts # Run specific file ``` ## Writing Tests ```typescript import "@tests/helpers/test-config" import { beforeAll, afterAll, test, expect } from 'bun:test' import { startTestServer } from '../helpers/server' import type { TestServer } from '../helpers/server' let testServer: TestServer beforeAll(async () => { testServer = await startTestServer() }) afterAll(async () => { await testServer.shutdown() }) test('health check returns ok', async () => { const response = await fetch(`${testServer.url}/health`) const data = await response.json() expect(data.status).toBe('ok') }) ``` --- # Background Jobs This page covers the internal implementation of the worker system. For adding custom jobs, see [Adding Jobs](./adding-jobs). ## Job Storage Jobs are stored in the [`workerJobs`](https://github.com/QuickDapp/QuickDapp/blob/v3.11.3/src/server/db/schema.ts) table with these key fields: | Field | Purpose | |-------|---------| | `tag` | Identifier for logging and debugging | | `type` | Job type name matching the registry | | `userId` | Associated user (0 for system jobs) | | `data` | JSON payload passed to the job runner | | `due` | When the job should run | | `started` | When execution began | | `finished` | When execution completed | | `success` | Whether the job succeeded | | `result` | Return value from the job runner | | `cronSchedule` | Cron expression for recurring jobs | | `persistent` | Whether job survives server restarts | | `autoRescheduleOnFailure` | Retry on failure | | `autoRescheduleOnFailureDelay` | Seconds to wait before retry | | `removeDelay` | Seconds to keep completed job | ## Worker Process Each worker runs as a forked child process with its own `ServerApp` instance. The [`start-worker.ts`](https://github.com/QuickDapp/QuickDapp/blob/v3.11.3/src/server/start-worker.ts) entry point handles: 1. Creating a `ServerApp` without the `WorkerManager` (to avoid recursive spawning) 2. Connecting to the database with a smaller connection pool 3. Starting the job polling loop 4. Graceful shutdown on SIGTERM Workers poll the database for jobs where `due <= now` and `started IS NULL`, using the `getNextPendingJob()` function to claim jobs atomically. ## IPC Communication Workers communicate with the main server through Node.js IPC. Message types are defined in [`ipc-types.ts`](https://github.com/QuickDapp/QuickDapp/blob/v3.11.3/src/server/workers/ipc-types.ts): - `WorkerStarted` — Worker process initialized - `WorkerShutdown` — Worker shutting down - `WorkerError` — Unhandled error in worker - `Heartbeat` — Keep-alive signal - `SendToUser` — Route WebSocket message to specific user - `Broadcast` — Send WebSocket message to all connected clients When a job needs to send a real-time notification, it uses [`serverApp.createNotification()`](https://github.com/QuickDapp/QuickDapp/blob/v3.11.3/src/server/bootstrap.ts) which saves to the database and sends an IPC message. The main server receives this and routes it through the `SocketManager`. ## Cron Scheduling Jobs with a `cronSchedule` field automatically reschedule after completion. The schedule uses standard cron syntax: ``` ┌───────────── second (0-59) │ ┌───────────── minute (0-59) │ │ ┌───────────── hour (0-23) │ │ │ ┌───────────── day of month (1-31) │ │ │ │ ┌───────────── month (1-12) │ │ │ │ │ ┌───────────── day of week (0-6) │ │ │ │ │ │ * * * * * * ``` The [`removeOldWorkerJobs`](https://github.com/QuickDapp/QuickDapp/blob/v3.11.3/src/server/workers/jobs/removeOldWorkerJobs.ts) job runs hourly to clean up completed job records. ## Error Handling When a job throws an error: 1. The `finished` timestamp and `success = false` are recorded 2. If `autoRescheduleOnFailure` is true, a new job is scheduled after the configured delay 3. The error is logged with the job context Jobs don't retry infinitely—the rescheduled job is a new record. The original failed job remains for debugging until the cleanup job removes it. ## Monitoring Job execution is logged with structured data including job ID, type, duration, and result. Enable debug logging with `WORKER_LOG_LEVEL=debug` for detailed execution traces. The `workerJobs` table serves as both queue and audit log. Query it to check pending jobs, recent failures, or execution patterns. --- # Adding Jobs To add a custom job type, create a job file, define its data type, and register it in the job registry. ## Create the Job File Jobs are defined in [`src/server/workers/jobs/`](https://github.com/QuickDapp/QuickDapp/blob/v3.11.3/src/server/workers/jobs/). Each job exports a `Job` object with a `run` function: ```typescript // src/server/workers/jobs/myCustomJob.ts import type { Job, JobParams } from "./types" export const myCustomJob: Job = { async run({ serverApp, log, job }: JobParams) { log.info("Starting custom job", { jobId: job.id }) // Access database const result = await serverApp.db.select().from(users) // Create notifications await serverApp.createNotification(job.userId, { type: "job_completed", message: "Your job finished" }) log.info("Job completed", { jobId: job.id }) } } ``` The `JobParams` object provides: - `serverApp` — Full access to database, notifications, and other services - `log` — Logger scoped to this job execution - `job` — The job record including `id`, `type`, `data`, `userId` ## Define Data Types Add your job's data type in [`src/server/workers/jobs/types.ts`](https://github.com/QuickDapp/QuickDapp/blob/v3.11.3/src/server/workers/jobs/types.ts): ```typescript export interface MyCustomJobData { targetUserId: number action: string } export type JobType = | "removeOldWorkerJobs" | "myCustomJob" // Add your type ``` ## Register the Job Add your job to the [`jobRegistry`](https://github.com/QuickDapp/QuickDapp/blob/v3.11.3/src/server/workers/jobs/registry.ts): ```typescript import { myCustomJob } from "./myCustomJob" export const jobRegistry: JobRegistry = { removeOldWorkerJobs: removeOldWorkerJobsJob, myCustomJob: myCustomJob, } ``` ## Submit Jobs Once registered, submit jobs from anywhere with `ServerApp` access: ```typescript await serverApp.workerManager.submitJob({ tag: "process-user-42", type: "myCustomJob", userId: currentUser.id, data: { targetUserId: 42, action: "sync" } }) ``` For more guidelines on writing reliable jobs, see [Best Practices](./best-practices). --- # Best Practices Guidelines for writing reliable, efficient background jobs. ## Idempotency Design jobs to be safely re-runnable. If a job fails mid-execution and gets rescheduled, it should handle duplicate processing gracefully: ```typescript async run({ serverApp, log, job }) { // Check if the work was already done const existing = await serverApp.db.select() .from(processedItems) .where(eq(processedItems.externalId, job.data.externalId)) .then(rows => rows[0]) if (existing) { log.info("Already processed, skipping") return } // Do the actual work await processItem(serverApp, job.data) } ``` Use unique constraints in the database to prevent duplicate records even if the check-then-act pattern has a race condition. ## Error Handling Let errors bubble up — the worker system records failures and handles retries based on job configuration. Log context before throwing so the error is debuggable: ```typescript async run({ serverApp, log, job }) { const result = await fetchExternalData(job.data.url) if (!result.ok) { log.error("External API returned error", { status: result.status, jobId: job.id }) throw new Error(`API returned ${result.status}`) } // Process result... } ``` ## Duration Keep jobs focused and short. For long-running work, break it into chains of smaller jobs: ```typescript // Instead of one massive job, chain them async run({ serverApp, log, job }) { const batch = await getNextBatch(serverApp, job.data.cursor) await processBatch(serverApp, batch) if (batch.hasMore) { // Schedule continuation await serverApp.workerManager.submitJob({ tag: `process-batch-${batch.nextCursor}`, type: "processBatch", userId: 0, data: { cursor: batch.nextCursor } }) } } ``` ## Database Transactions Use `withTransaction` for operations that need atomicity. The transaction wrapper handles serialization conflicts automatically with retries: ```typescript import { withTransaction } from "../../db/shared" async run({ serverApp, log, job }) { await withTransaction(serverApp.db, async (tx) => { const [user] = await tx.insert(users).values({}).returning() await tx.insert(userAuth).values({ userId: user.id, authType: "email", authIdentifier: job.data.email }) }) } ``` ## Monitoring Use structured logging with job IDs for tracing. The `log` parameter is already scoped to the job: ```typescript log.info("Processing started", { itemCount: items.length }) log.debug("Item details", { item }) log.info("Processing complete", { processed: items.length }) ``` Check the `workerJobs` table to monitor execution patterns: ```sql -- Recent failures SELECT type, tag, result, finished FROM worker_jobs WHERE success = false ORDER BY finished DESC LIMIT 20; -- Average execution time by type SELECT type, AVG(EXTRACT(EPOCH FROM (finished - started))) as avg_seconds FROM worker_jobs WHERE finished IS NOT NULL GROUP BY type; ``` ## Resource Management Worker processes use smaller database connection pools (2 connections vs 10 for the main server). Be mindful of concurrent database operations within a job — avoid opening many parallel queries. ## Job Deduplication Use tags to prevent scheduling duplicate jobs: ```typescript // Check if a job with this tag is already pending const existing = await serverApp.db.select() .from(workerJobs) .where(and( eq(workerJobs.tag, `sync-user-${userId}`), isNull(workerJobs.started) )) .then(rows => rows[0]) if (!existing) { await serverApp.workerManager.submitJob({ tag: `sync-user-${userId}`, type: "syncUser", userId, data: {} }) } ``` ## Cron Scheduling For recurring jobs, use cron expressions. Keep frequency appropriate for the task — polling too often wastes resources: ```typescript await serverApp.workerManager.submitJob({ tag: "cleanup", type: "removeOldWorkerJobs", userId: 0, cronSchedule: "0 0 * * * *", // Every hour persistent: true }) ``` The `persistent` flag ensures the job survives server restarts. Without it, cron jobs need to be re-submitted on startup. ## Testing Test jobs in isolation using the test helpers: ```typescript import { startTestServer } from "../helpers/server" test("my job processes correctly", async () => { const { serverApp } = await startTestServer() // Set up test data await serverApp.db.insert(items).values({ ... }) // Run the job directly const log = serverApp.createLogger("test") await myJob.run({ serverApp, log, job: { id: 1, type: "myJob", data: { ... }, userId: 0 } }) // Verify results const result = await serverApp.db.select().from(items) expect(result).toHaveLength(1) }) ``` --- # Worker QuickDapp's worker system handles background jobs through child processes and a database-backed queue. Jobs run independently of HTTP requests, making them suitable for tasks that shouldn't block the request-response cycle. ## Why Workers? Many operations don't belong inside an HTTP request handler: - **Sending emails** — Delivering verification codes or notifications without blocking the API response - **Processing uploaded files** — Parsing, transforming, or importing data in the background - **Scheduled maintenance** — Cleaning old data, generating reports, running periodic checks - **Monitoring external services** — Polling APIs or other systems for changes - **Long-running computations** — Any task that takes too long for a request-response cycle The worker system lets you schedule these as jobs that run independently, with automatic retry, cron scheduling, and full access to the database and other services. ## Architecture The [`WorkerManager`](https://github.com/QuickDapp/QuickDapp/blob/v3.11.3/src/server/workers/index.ts) spawns child processes that poll the `workerJobs` table for pending work. Each worker gets a full `ServerApp` instance with database access and logging. Workers communicate with the main server through IPC messages. When a worker needs to send a WebSocket notification, it sends an IPC message that the main process routes through the `SocketManager`. This allows workers to trigger real-time updates without direct socket access. The number of workers is configurable via `WORKER_COUNT`. Set it to `cpus` for auto-scaling based on CPU cores, or a specific number for fixed worker count. ## Built-in Jobs One job type comes pre-configured: [`removeOldWorkerJobs`](https://github.com/QuickDapp/QuickDapp/blob/v3.11.3/src/server/workers/jobs/removeOldWorkerJobs.ts) cleans up completed jobs from the database. It runs on a cron schedule to prevent table bloat. :::note Variants may add additional built-in jobs. For example, the [Web3 variant](../variants/web3) adds blockchain event monitoring and contract deployment jobs. ::: ## Job Lifecycle Jobs flow through these states: 1. **Scheduled** — Job inserted into `workerJobs` with a `due` timestamp 2. **Started** — Worker picks up the job and sets `started` timestamp 3. **Finished** — Job completes with `finished` timestamp and `success` flag 4. **Removed** — Cleanup job deletes old completed entries Jobs can be configured for automatic rescheduling on failure with configurable delays. Cron-scheduled jobs automatically reschedule themselves after completion. ## Submitting Jobs Submit jobs through the `WorkerManager`: ```typescript await serverApp.workerManager.submitJob({ tag: "my-job", type: "myCustomJob", userId: user.id, data: { customField: "value" } }) ``` The `tag` field identifies the job for logging and debugging. The `type` must match a registered job in the [`jobRegistry`](https://github.com/QuickDapp/QuickDapp/blob/v3.11.3/src/server/workers/jobs/registry.ts). ## Configuration ```bash WORKER_COUNT=cpus # Number of workers ('cpus' or integer) WORKER_LOG_LEVEL=info # Log level for worker processes ``` See [Adding Jobs](./adding-jobs) for creating custom job types, [Background Jobs](./background-jobs) for implementation details, and [Best Practices](./best-practices) for guidelines on writing reliable jobs. --- # Getting started ## Step 0 - Pre-requisites Ensure you have the following pre-requisites installed and ready: * [Bun](https://bun.sh/) * [Docker](http://docker.com/) * [Git](https://git-scm.com/) This guide assumes you have some knowledge of the basics of web application development (e.g backend, frontend, database, CLI, etc). However, if you follow the instructions properly you should still be able to get things up and running even if you don't understand everything immediately. ## Step 1 - Create a new project Open a terminal window and type in: ```shell bunx @quickdapp/cli create my-project ``` The above command will get the QuickDapp CLI and use it to create a new project in a folder called `my-project`. You can now enter the `my-project` folder using: ```shell cd my-project ``` _Note: The following steps must be be executed within the `my-project` project folder._ ## Step 2 - Run the database server Now we'll use [Docker compose]() to install and run a temporary Postgres database. This will be the database that QuickDapp will use when run locally: ```shell docker compose up -d ``` You should see output which looks like this: ``` [+] up 2/2 ✔ Network my-project_default Created 0.0s ✔ Container quickdapp-postgres Created 0.1s ``` From this point on the database is running in the background. To shutdown the database at any time run: ```shell docker compose down ``` You can now connect to the database yourself through a third-party client (e.g [DBeaver](https://dbeaver.io/)). You can see the full database connection parameters by looking at the [`.env`](https://github.com/QuickDapp/QuickDapp/blob/v3.11.3/packages/base/.env) file in the project. This file defines the [environment variables](./environment-variables) used by the web app._ ## Step 3 - Setup database tables With the database server running we need to create our database tables, ready for use by the QuickDapp backend. Run: ```shell bun run db push ``` You may be prompted to confirm the execution of SQL statements against the database. ## Step 4 - Run dev server Now we're ready to run the dev server and see the demo web page in a browser. Run: ```shell bun run dev ``` You will see output like the following: ``` VITE v6.4.1 ready in 1803 ms ➜ Local: http://localhost:5173/ ➜ Network: use --host to expose 2026-01-29T05:08:33.934Z [info] 🚀 QuickDapp server v3.4.0 started in 50.77ms 2026-01-29T05:08:33.934Z [info] ➜ Running at: http://localhost:3000/ 2026-01-29T05:08:33.934Z [info] ➜ GraphQL endpoint: http://localhost:3000/graphql 2026-01-29T05:08:33.934Z [info] ➜ Environment: development ``` You can now access the server in your browser at two URLs: * [http://localhost:5173/](http://localhost:5173/) * This is a server which bundles and serves up your frontend code as a web app. * [http://localhost:3000/](http://localhost:3000/) * This is the backend server which the above web app talks to. If you access [http://localhost:5173/](http://localhost:5173/) you will see something which looks like this: (image: https://raw.githubusercontent.com/quickdapp/quickdapp/v3.11.3/packages/docs/images/demopage.png) Whereas if you access [http://localhost:3000/](http://localhost:3000/) you will see: (image: https://raw.githubusercontent.com/quickdapp/quickdapp/v3.11.3/packages/docs/images/serverpage.png) The two links shown on this page are: * `/graphql` - A web interface for accessing and querying the backend [GraphQL API](./backend/graphql). * `/health` - A simple API which returns a basic health-check for the server. :::note If you want to monitor your QuickDapp app's uptime you would typically monitor the `/health` endpoint. ::: ## Step 5 - Hot reloading You can now edit the code of your app and immediately see the changes reflected in the browser. Go ahead and change the text in `HomePage.tsx` to be something different and you should see the page in the browser immediately update. The same goes for if you change any of the backend server code - you will see the dev server running in the terminal auto-reload any code changes. It is only if you change the `.env` file settings that you will need to manually restart the dev server. ## Step 6 - Ready! At this point everything is ready for you to actually develop your app. You may wish to follow one of our [tutorials](./tutorials). The remainder of this guide helps you get this basic app deployed to production in the cloud. ## Step 7 - Setup Github QuickDapp comes with [Github workflows](./deployment/github-workflows) that make it easy to test your web app as well as build it as a Docker container for easy cloud deployment. Let's get this setup: First, get a [Github.com](https://github.com) account (it's free!). Once you're logged in, create a new repository on Github called `my-project`: (image: https://raw.githubusercontent.com/quickdapp/quickdapp/v3.11.3/packages/docs/images/github-new-repo.png) You can set its visibility to _Private_: (image: https://raw.githubusercontent.com/quickdapp/quickdapp/v3.11.3/packages/docs/images/github-repo-private.png) Now copy the SSH repository URL for your repo: (image: https://raw.githubusercontent.com/quickdapp/quickdapp/v3.11.3/packages/docs/images/github-ssh-url.png) Now run the following locally in your project folder (replace `<...>` with the right values): ```shell git add . git commit -am "chore: initial version" git remote add origin git push --set-upstream origin main ``` Now you should see the QuickDapp code show up in your new Github repository: (image: https://raw.githubusercontent.com/quickdapp/quickdapp/v3.11.3/packages/docs/images/github-initial-commit.png) ## Step 8 - Run Github workflow Now we're ready to run the Github workflow to build the production-ready version of our web app. Goto your Github repository's _Actions_ tab and select the _Docker Build and Push_ worfklow from the left. Then choose to run the workflow against the `main` branch: (image: https://raw.githubusercontent.com/quickdapp/quickdapp/v3.11.3/packages/docs/images/github-run-docker-workflow.png) Refresh the page and you should see the workflow running. Once it has completed successfully a Docker image should have been created. To see this package goto your Github's repository homepage and look at the right-hand side: (image: https://raw.githubusercontent.com/quickdapp/quickdapp/v3.11.3/packages/docs/images/github-package-list.png) If you click into the package generated in the previous step you will see something like this, with the package marked as _Private_: (image: https://raw.githubusercontent.com/quickdapp/quickdapp/v3.11.3/packages/docs/images/github-package-overview.png) ## Step 9 - Generate Github access token In order to deploy your newly built Docker image you will need to authenticate access to it. To do this goto https://github.com/settings/tokens/new. Create a new token with the `read:packages` permission set: (image: https://raw.githubusercontent.com/quickdapp/quickdapp/v3.11.3/packages/docs/images/github-new-pat.png) Copy and paste the generated token value somewhere (you will only be shown in once!). ## Step 10 - Setup production database You will need a PostgreSQL database for production use. You can use any PostgreSQL hosting service such as: * [DigitalOcean Managed Databases](https://www.digitalocean.com/products/managed-databases) * [AWS RDS](https://aws.amazon.com/rds/) * [Railway](https://railway.app/) * [Supabase](https://supabase.com/) ## Step 11 - TODO ## Next steps Now that you have QuickDapp running, explore the documentation to learn about: * [Backend architecture](./backend/) - Understanding the ServerApp pattern and database layer * [Frontend development](./frontend/) - Building React components * [Worker system](./worker/) - Adding background jobs and cron tasks * [Command line tools](./command-line/) - Development and deployment commands * [Variants](./variants/) - Specialized derivations like the Web3 variant --- # GitHub Workflows QuickDapp comes with GitHub Actions workflows for CI/CD automation. ## Docker Build and Push The primary workflow builds a Docker container image and pushes it to GitHub Container Registry (GHCR). This is the recommended approach for deploying QuickDapp to cloud platforms. **Trigger**: Manual dispatch from the Actions tab (workflow_dispatch), targeting any branch. **What it does**: 1. Checks out the repository 2. Logs into GitHub Container Registry 3. Builds the Docker image using the project's `Dockerfile` 4. Tags the image with `latest` and the git SHA 5. Pushes the image to `ghcr.io//` **Usage**: 1. Go to your repository's **Actions** tab 2. Select **Docker Build and Push** from the left sidebar 3. Click **Run workflow** and choose the branch 4. Wait for the build to complete 5. Find the built image under your repository's **Packages** section The built image can be deployed to any container platform (DigitalOcean App Platform, AWS ECS, Railway, Fly.io, etc.). See [Getting Started](../getting-started) for a step-by-step deployment walkthrough. ## Setting Up The workflow uses `GITHUB_TOKEN` for GHCR authentication, which is automatically provided by GitHub Actions. No additional secrets are needed for the basic workflow. For deployment to external platforms, you may need to add platform-specific secrets in your repository's **Settings > Secrets and variables > Actions**. --- # Binary Deployment QuickDapp builds self-contained executables that include the server, all dependencies, and static assets. Upload the single file to your server and run it—no Node.js or Bun runtime required. ## Building ```shell bun run build ``` This creates binaries for multiple platforms in `dist/binaries/`: - `quickdapp-linux-x64` — Linux servers (x64) - `quickdapp-linux-arm64` — Linux servers (ARM64) - `quickdapp-darwin-x64` — macOS (Intel) - `quickdapp-darwin-arm64` — macOS (Apple Silicon) - `quickdapp-windows-x64.exe` — Windows ## Running Make the binary executable and run it: ```shell chmod +x quickdapp-linux-x64 NODE_ENV=production ./quickdapp-linux-x64 ``` The binary reads environment variables from `.env`, `.env.production`, and `.env.production.local` in order. ## Server Deployment Upload the binary and environment file to your server: ```shell scp dist/binaries/quickdapp-linux-x64 user@server:/opt/quickdapp/ scp .env.production user@server:/opt/quickdapp/.env ssh user@server cd /opt/quickdapp chmod +x quickdapp-linux-x64 NODE_ENV=production ./quickdapp-linux-x64 ``` For background execution: ```shell nohup NODE_ENV=production ./quickdapp-linux-x64 > quickdapp.log 2>&1 & ``` ## Platform Notes **macOS**: You may need to remove the quarantine attribute on first run: ```shell xattr -d com.apple.quarantine quickdapp-darwin-x64 ``` **Windows**: Run from Command Prompt with environment variables: ```cmd set NODE_ENV=production quickdapp-windows-x64.exe ``` ## Security Set proper file permissions: ```shell chmod 750 quickdapp-linux-x64 # Execute for owner/group only chmod 640 .env # Read for owner/group only ``` Consider running as a dedicated non-root user: ```shell sudo useradd -r -s /bin/false quickdapp sudo chown quickdapp:quickdapp quickdapp-linux-x64 sudo -u quickdapp ./quickdapp-linux-x64 ``` --- # Docker Deployment QuickDapp can be deployed using Docker containers. The Dockerfile uses pre-built binaries for a minimal image size. ## Building the Image Build the application first, then create the Docker image: ```shell bun run build docker build -t quickdapp:latest . ``` ## Running Run with an environment file: ```shell docker run -d \ --name quickdapp \ -p 3000:3000 \ --env-file .env.production \ quickdapp:latest ``` Or pass environment variables directly: ```shell docker run -d \ --name quickdapp \ -p 3000:3000 \ -e DATABASE_URL="postgresql://user:pass@host:5432/db" \ -e SESSION_ENCRYPTION_KEY="your_32_character_key" \ quickdapp:latest ``` ## Container Registry Push to a registry for deployment: ```shell docker tag quickdapp:latest your-registry.com/quickdapp:latest docker push your-registry.com/quickdapp:latest ``` ## Monitoring ```shell # View logs docker logs quickdapp # Monitor resources docker stats quickdapp # Health check curl http://localhost:3000/health ``` ## Multi-Platform Builds Build for multiple architectures: ```shell docker buildx build \ --platform linux/amd64,linux/arm64 \ -t quickdapp:latest \ --push . ``` --- # Deployment QuickDapp supports two deployment strategies: self-contained binaries and Docker containers. Both provide simple, reliable deployment with minimal server requirements. ## Deployment Options **Binary Deployment** creates standalone executables that include all dependencies and assets. No runtime is needed on the server—just upload and run. This is the recommended approach for most deployments. **Docker Deployment** packages the application in containers for orchestration environments. Use this when your infrastructure is already containerized. See [Binary Deployment](./binary) and [Docker Deployment](./docker) for detailed instructions. ## Quick Start ```shell # Build the application bun run build # Run migrations NODE_ENV=production bun run db migrate # Deploy binary ./dist/binaries/quickdapp-linux-x64 # Or deploy with Docker docker build -t quickdapp:latest . docker run -d -p 3000:3000 --env-file .env.production quickdapp:latest ``` ## Environment Configuration Create `.env.production` with your production settings: ```bash DATABASE_URL=postgresql://user:password@host:5432/database SESSION_ENCRYPTION_KEY=your_32_character_encryption_key # Web3 (optional) WEB3_ENABLED=true WEB3_SERVER_WALLET_PRIVATE_KEY=0xYourWalletPrivateKey CHAIN=sepolia WEB3_SEPOLIA_RPC=https://sepolia.infura.io/v3/your-api-key WEB3_FACTORY_CONTRACT_ADDRESS=0xYourContractAddress ``` ## Infrastructure **Minimum requirements**: 1 CPU core, 512MB RAM, 1GB disk for testing; 2 cores, 1GB RAM for production. **Database**: PostgreSQL 11 or higher. Use a managed service (AWS RDS, DigitalOcean, Railway, Supabase) for production. **HTTPS**: Use a reverse proxy like Nginx for SSL termination. The WebSocket endpoint at `/ws` requires proper upgrade headers. ## Health Monitoring The server provides a health endpoint: ```shell curl http://localhost:3000/health ``` Configure your monitoring system to check this endpoint for uptime alerts. --- # GraphQL QuickDapp exposes its API through GraphQL Yoga integrated with ElysiaJS. The schema uses a custom `@auth` directive to protect operations that require authentication. ## Schema Overview The API provides authentication and notification management. There are no GraphQL subscriptions—real-time updates happen through WebSockets instead. **Queries** include token validation (public), fetching the current user profile (authenticated), fetching notifications (authenticated), and getting unread counts (authenticated). **Mutations** handle email verification, OAuth login URLs, and notification management. The full schema is defined in [`src/shared/graphql/schema.ts`](https://github.com/QuickDapp/QuickDapp/blob/v3.11.3/src/shared/graphql/schema.ts). Key types: ```graphql type Notification { id: PositiveInt! userId: PositiveInt! data: JSON! createdAt: DateTime! read: Boolean! } type AuthResult { success: Boolean! token: String profile: UserProfile error: String } type Query { validateToken: ValidateTokenResult! me: UserProfile! @auth getMyNotifications(pageParam: PageParam!): NotificationsResponse! @auth getMyUnreadNotificationsCount: Int! @auth } type Mutation { sendEmailVerificationCode(email: String!): EmailVerificationResult! authenticateWithEmail(email: String!, code: String!, blob: String!): AuthResult! getOAuthLoginUrl(provider: OAuthProvider!, redirectUrl: String): OAuthLoginUrlResult! markNotificationAsRead(id: PositiveInt!): Success! @auth markAllNotificationsAsRead: Success! @auth } ``` :::note Variants may extend the schema with additional operations. For example, the [Web3 variant](../variants/web3) adds `generateSiweMessage` and `authenticateWithSiwe` mutations. ::: ## The @auth Directive Operations marked with `@auth` require a valid JWT in the Authorization header. The GraphQL handler extracts auth requirements at startup and checks them before running resolvers. When an unauthenticated request tries to access a protected operation, it returns a GraphQL error with `extensions.code = "UNAUTHORIZED"`. Mixed queries containing both public and protected fields fail entirely when unauthenticated—no partial data is returned. ## Resolver Implementation Resolvers are defined in [`src/server/graphql/resolvers.ts`](https://github.com/QuickDapp/QuickDapp/blob/v3.11.3/src/server/graphql/resolvers.ts). Each resolver receives the context containing `ServerApp` and the authenticated user (if any): ```typescript const resolvers = { Query: { getMyNotifications: async (_, { pageParam }, context) => { const user = getAuthenticatedUser(context) return await getNotifications(context.serverApp.db, user.id, pageParam) } } } ``` All resolvers are wrapped with `withSpan` for Sentry performance tracking. Database errors return with `extensions.code = "DATABASE_ERROR"`, authentication failures with `"AUTHENTICATION_FAILED"`, and disabled accounts with `"ACCOUNT_DISABLED"`. ## No Field Resolvers QuickDapp deliberately avoids GraphQL field resolvers. All data is fetched in the parent resolver using SQL joins, which prevents N+1 query problems and keeps performance predictable. ## Client Integration The frontend uses `graphql-request` with the queries and mutations defined in [`src/shared/graphql/queries.ts`](https://github.com/QuickDapp/QuickDapp/blob/v3.11.3/src/shared/graphql/queries.ts) and [`mutations.ts`](https://github.com/QuickDapp/QuickDapp/blob/v3.11.3/src/shared/graphql/mutations.ts). The GraphQL client is a singleton that includes the auth token when set. See [`src/server/graphql/index.ts`](https://github.com/QuickDapp/QuickDapp/blob/v3.11.3/src/server/graphql/index.ts) for the handler setup and [`src/shared/graphql/schema.ts`](https://github.com/QuickDapp/QuickDapp/blob/v3.11.3/src/shared/graphql/schema.ts) for the complete schema definition. --- # Sentry QuickDapp integrates with [Sentry](https://sentry.io/) for error tracking and performance monitoring. When configured, Sentry captures unhandled exceptions, logs errors, and traces performance across server and worker processes. ## Configuration | Variable | Required | Default | Description | |----------|----------|---------|-------------| | `SENTRY_DSN` | No | — | Sentry DSN for the main server process | | `SENTRY_WORKER_DSN` | No | — | Sentry DSN for worker processes (can be same or different project) | | `SENTRY_TRACES_SAMPLE_RATE` | No | `0` | Fraction of requests to trace (0.0 to 1.0) | | `SENTRY_PROFILE_SESSION_SAMPLE_RATE` | No | `0` | Fraction of sessions to profile (0.0 to 1.0) | Set `SENTRY_DSN` to enable Sentry for the main server. Set `SENTRY_WORKER_DSN` to enable it for background workers. If you only set one, only that process type will report to Sentry. ## Initialization Sentry initializes during server and worker startup before any other code runs: - **Server**: Initialized in `start-server.ts` if `SENTRY_DSN` is set - **Worker**: Initialized in `start-worker.ts` if `SENTRY_WORKER_DSN` is set ```typescript initializeSentry({ dsn: serverConfig.SENTRY_DSN, environment: serverConfig.NODE_ENV, tracesSampleRate: serverConfig.SENTRY_TRACES_SAMPLE_RATE, profileSessionSampleRate: serverConfig.SENTRY_PROFILE_SESSION_SAMPLE_RATE, }) ``` ## User Context Link errors to specific users with `setSentryUser()` and `clearSentryUser()`. These functions update the Sentry scope so all subsequent errors include user information: ```typescript import { setSentryUser, clearSentryUser } from "./lib/sentry" // After authentication succeeds setSentryUser({ id: user.id }) // After logout clearSentryUser() ``` When a user is set, Sentry events include their ID, making it easier to trace issues affecting specific accounts. ## Performance Tracing Use `startSpan()` to trace performance-sensitive operations. The function is available on `ServerApp` and wraps your code in a Sentry span: ```typescript const result = await serverApp.startSpan("fetchUserData", async (span) => { // Your code here return await db.query.users.findFirst({ where: eq(users.id, userId) }) }) ``` Spans appear in Sentry's Performance dashboard, showing execution time and call hierarchies. Set `SENTRY_TRACES_SAMPLE_RATE` to control what fraction of requests are traced. ## Log Transport The `SentryTransport` class bridges the `@hiddentao/logger` library with Sentry's logging system. When Sentry is configured, this transport is automatically added to the root logger: ```typescript if (serverConfig.SENTRY_DSN) { logger.addTransport(new SentryTransport()) } ``` By default, the transport sends ERROR level logs and above to Sentry. You can configure the minimum level: ```typescript new SentryTransport({ minLevel: LogLevel.WARN }) ``` Logs appear in Sentry with their category and metadata preserved. ## Error Capture QuickDapp captures errors at multiple levels: **Uncaught Exceptions and Unhandled Rejections** Both server and worker processes register handlers that capture exceptions and rejections before exiting: ```typescript process.on("uncaughtException", (error) => { logger.error("Uncaught exception:", error) Sentry.captureException(error) process.exit(1) }) ``` **Elysia Error Handler** The server's global error handler captures unexpected errors during request processing: ```typescript app.onError(({ error, set, request }) => { Sentry.captureException(error) set.status = 500 return { error: "Internal server error" } }) ``` This ensures errors that escape your handlers still reach Sentry before returning a 500 response. ## Graceful Shutdown When the server or worker shuts down, `Sentry.close()` flushes pending events: ```typescript if (serverConfig.SENTRY_DSN) { await Sentry.close(2000) // Wait up to 2 seconds logger.info("Sentry events flushed") } ``` This ensures errors captured just before shutdown are not lost. --- # Bootstrap The bootstrap process creates the `ServerApp` object that gets passed throughout the application. This happens once at startup and provides every component with access to shared services like the database, logger, and WebSocket manager. ## How Startup Works When the server starts, [`src/server/index.ts`](https://github.com/QuickDapp/QuickDapp/blob/v3.11.3/src/server/index.ts) checks the `WORKER_ID` environment variable. If set, it runs as a worker process. Otherwise, it starts the main HTTP server. The main server path calls `createApp()` in [`start-server.ts`](https://github.com/QuickDapp/QuickDapp/blob/v3.11.3/src/server/start-server.ts), which initializes services in order: 1. **Sentry** — Error tracking and performance monitoring (if configured) 2. **SocketManager** — WebSocket connection handling 3. **WorkerManager** — Spawns worker child processes 4. **ServerApp** — Created via `createServerApp()` in [`bootstrap.ts`](https://github.com/QuickDapp/QuickDapp/blob/v3.11.3/src/server/bootstrap.ts) 5. **ElysiaJS** — HTTP server with GraphQL, health checks, static files Worker processes follow a simpler path. They create a `ServerApp` without the worker manager (to avoid circular spawning) and run the job polling loop. ## The createServerApp Function The core bootstrap logic lives in [`src/server/bootstrap.ts`](https://github.com/QuickDapp/QuickDapp/blob/v3.11.3/src/server/bootstrap.ts). It connects to the database and assembles everything into the `ServerApp` object: ```typescript export async function createServerApp(options: { includeWorkerManager?: boolean workerCountOverride?: number socketManager: ISocketManager rootLogger: Logger }): Promise { const db = await dbManager.connect() // Notification helper that persists to DB and sends via WebSocket const createNotification = async (userId: number, data: NotificationData) => { // Inserts to DB and sends via WebSocket } const baseServerApp = { db, rootLogger, createLogger, startSpan, ... } if (includeWorkerManager) { return { ...baseServerApp, workerManager: await createWorkerManager(...) } } return baseServerApp } ``` ## Using ServerApp GraphQL resolvers receive `ServerApp` through their context. The GraphQL handler builds the context for each request: ```typescript // In src/server/graphql/index.ts const context = { serverApp, user: authenticatedUser, // null if not authenticated operationName, requiresAuth } ``` Resolvers then access services through this context: ```typescript const resolvers = { Query: { getMyNotifications: async (_, { limit, offset }, context) => { const { serverApp, user } = context return getNotifications(serverApp.db, user.id, limit, offset) } } } ``` Worker jobs receive `ServerApp` as their first parameter along with job data: ```typescript export const run: JobRunner = async ({ serverApp, log, job }) => { // Full access to database, notifications, etc. const result = await serverApp.db.select().from(settings) log.info("Job completed", { result }) } ``` ## Configuration The bootstrap process loads configuration through a layered system: 1. `.env` — Base configuration, committed to git 2. `.env.{NODE_ENV}` — Environment-specific overrides 3. `.env.local` — Local developer overrides, gitignored Access configuration through the typed [`serverConfig`](https://github.com/QuickDapp/QuickDapp/blob/v3.11.3/src/shared/config/server.ts) object. Never read `process.env` directly in application code—this ensures type safety and consistent defaults. ## Testing Tests create their own `ServerApp` with the worker manager disabled: ```typescript const serverApp = await createServerApp({ socketManager: createTestSocketManager(), workerManager: undefined }) ``` The test helpers in [`tests/helpers/`](https://github.com/QuickDapp/QuickDapp/blob/v3.11.3/tests/helpers/) provide utilities for creating test servers, managing database state, and cleaning up between tests. See [`src/server/bootstrap.ts`](https://github.com/QuickDapp/QuickDapp/blob/v3.11.3/src/server/bootstrap.ts) for the complete implementation and [`src/server/types.ts`](https://github.com/QuickDapp/QuickDapp/blob/v3.11.3/src/server/types.ts) for the `ServerApp` type definition. --- # Error Handling QuickDapp uses a structured error system with typed error classes on the server and consistent error codes in the GraphQL API. In production, error details are masked to prevent information leakage. Sentry captures errors automatically when configured. ## Error Classes All custom errors extend `ApplicationError`, which adds a `code` and optional `metadata` to the standard `Error`. These classes are defined in [`src/server/lib/errors.ts`](https://github.com/QuickDapp/QuickDapp/blob/v3.11.3/src/server/lib/errors.ts): | Class | GraphQL Code | When to Use | |-------|-------------|-------------| | `ValidationError` | `INVALID_INPUT` | User input fails validation | | `AuthenticationError` | `AUTHENTICATION_FAILED` | Credentials are incorrect | | `DatabaseError` | `DATABASE_ERROR` | Database operation fails | | `NotFoundError` | `NOT_FOUND` | Requested resource doesn't exist | | `ExternalServiceError` | `INTERNAL_ERROR` | Third-party API call fails | | `AccountDisabledError` | `ACCOUNT_DISABLED` | Disabled user tries to access protected operation | Usage in resolvers: ```typescript import { ValidationError, NotFoundError } from "../lib/errors" if (!email) { throw new ValidationError("Email is required") } const user = await findUser(db, userId) if (!user) { throw new NotFoundError("User not found", { userId }) } ``` ## GraphQL Error Codes Error codes are defined in [`src/shared/graphql/errors.ts`](https://github.com/QuickDapp/QuickDapp/blob/v3.11.3/src/shared/graphql/errors.ts) and available on both client and server: ```typescript enum GraphQLErrorCode { UNAUTHORIZED = "UNAUTHORIZED", AUTHENTICATION_FAILED = "AUTHENTICATION_FAILED", INVALID_SIGNATURE = "INVALID_SIGNATURE", ACCOUNT_DISABLED = "ACCOUNT_DISABLED", OAUTH_CONFIG_ERROR = "OAUTH_CONFIG_ERROR", OAUTH_PROVIDER_ERROR = "OAUTH_PROVIDER_ERROR", OAUTH_STATE_INVALID = "OAUTH_STATE_INVALID", DATABASE_ERROR = "DATABASE_ERROR", NOT_FOUND = "NOT_FOUND", INVALID_INPUT = "INVALID_INPUT", INTERNAL_ERROR = "INTERNAL_ERROR", } ``` The client receives these in `error.response.errors[0].extensions.code`, making it straightforward to handle specific error types in the frontend. ## Error Masking In development, full error messages and stack traces are returned in GraphQL responses for debugging. In production, errors from `ApplicationError` subclasses return their message and code, while unexpected errors are masked to prevent leaking implementation details. ## Sentry Integration When `SENTRY_DSN` is configured, errors are automatically captured by Sentry with: - **User context** — The authenticated user ID is attached to error reports - **Performance tracing** — Database operations and resolver execution are traced via `startSpan()` - **Worker errors** — Worker processes have their own Sentry DSN (`SENTRY_WORKER_DSN`) for separate error tracking Configuration: ```bash SENTRY_DSN=https://...@sentry.io/... SENTRY_WORKER_DSN=https://...@sentry.io/... SENTRY_TRACES_SAMPLE_RATE=1.0 SENTRY_PROFILE_SESSION_SAMPLE_RATE=1.0 ``` ## Adding Custom Error Types Extend `ApplicationError` with a GraphQL error code: ```typescript import { GraphQLErrorCode } from "../../shared/graphql/errors" import { ApplicationError } from "./errors" export class RateLimitError extends ApplicationError { constructor(message = "Too many requests", metadata?: Record) { super(message, GraphQLErrorCode.INTERNAL_ERROR, metadata) } } ``` To add a new error code, add it to the `GraphQLErrorCode` enum in `src/shared/graphql/errors.ts` — this makes it available to both client and server code. See [`src/server/lib/errors.ts`](https://github.com/QuickDapp/QuickDapp/blob/v3.11.3/src/server/lib/errors.ts) for the error class implementations and [`src/shared/graphql/errors.ts`](https://github.com/QuickDapp/QuickDapp/blob/v3.11.3/src/shared/graphql/errors.ts) for the error code enum. --- # Database QuickDapp uses DrizzleORM with PostgreSQL. DrizzleORM provides type-safe queries that compile to efficient SQL, with full TypeScript integration so your IDE catches errors before they reach production. ## Schema The schema is defined in TypeScript at [`src/server/db/schema.ts`](https://github.com/QuickDapp/QuickDapp/blob/v3.11.3/src/server/db/schema.ts). QuickDapp includes four core tables: **users** stores user accounts. Authentication methods are stored separately in the `userAuth` table, so a single user can have multiple ways to sign in (email, OAuth). **userAuth** links authentication methods to users. Each row represents one auth method with its type (like "email" or "google") and identifier (the email address or provider user ID). The unique constraint on `authIdentifier` prevents duplicate registrations. Variants may add additional auth types (e.g. the Web3 variant adds wallet-based authentication). **notifications** stores user notifications with JSON data and read status. The application creates notifications through `ServerApp` and delivers them in real-time via WebSocket. **workerJobs** manages background job scheduling. Jobs have a due time, optional cron schedule for recurring execution, and fields for tracking status and results. ```typescript // Core tables from src/server/db/schema.ts export const users = pgTable("users", { id: serial("id").primaryKey(), disabled: boolean("disabled").default(false).notNull(), settings: json("settings"), createdAt: timestamp("created_at", { withTimezone: true }).defaultNow().notNull(), updatedAt: timestamp("updated_at", { withTimezone: true }).defaultNow().notNull(), }) export const userAuth = pgTable("user_auth", { id: serial("id").primaryKey(), userId: integer("user_id").references(() => users.id, { onDelete: "cascade" }).notNull(), authType: text("auth_type").notNull(), authIdentifier: text("auth_identifier").unique().notNull(), createdAt: timestamp("created_at", { withTimezone: true }).defaultNow().notNull(), updatedAt: timestamp("updated_at", { withTimezone: true }).defaultNow().notNull(), }) ``` ## Queries Access the database through `serverApp.db`. DrizzleORM queries read like SQL but with full type checking: ```typescript // Find user by auth identifier const authRecord = await serverApp.db .select() .from(userAuth) .where(eq(userAuth.authIdentifier, emailAddress)) .then(rows => rows[0]) // Get notifications for a user with pagination const userNotifications = await serverApp.db .select() .from(notifications) .where(eq(notifications.userId, userId)) .orderBy(desc(notifications.createdAt)) .limit(20) .offset(page * 20) // Count unread notifications const [{ count }] = await serverApp.db .select({ count: sql`count(*)` }) .from(notifications) .where(and( eq(notifications.userId, userId), eq(notifications.read, false) )) ``` ## Transactions QuickDapp uses PostgreSQL's SERIALIZABLE isolation level with automatic retry on serialization conflicts. The [`withTransaction`](https://github.com/QuickDapp/QuickDapp/blob/v3.11.3/src/server/db/shared.ts) helper handles this: ```typescript import { withTransaction } from "./db/shared" await withTransaction(serverApp.db, async (tx) => { // Create user const [user] = await tx.insert(users).values({}).returning() // Create auth record await tx.insert(userAuth).values({ userId: user.id, authType: "email", authIdentifier: emailAddress.toLowerCase() }) // If anything fails, entire transaction rolls back }) ``` The transaction wrapper automatically retries up to 7 times when PostgreSQL reports a serialization conflict. This approach avoids `FOR UPDATE` row locking while still preventing race conditions. ## Connection Management The database connection is managed through a singleton in [`src/server/db/connection.ts`](https://github.com/QuickDapp/QuickDapp/blob/v3.11.3/src/server/db/connection.ts). The `DatabaseConnectionManager` handles connection pooling and ensures only one pool exists per process. Pool sizes vary by context: 10 connections for the main server, 2 per worker process, and 1 for tests. The manager tracks global state to prevent connection leaks during test cleanup. ## Migrations Schema changes go through DrizzleORM's migration system: ```bash # Generate migration from schema changes bun run gen # Apply migrations (production) bun run db migrate # Push schema directly (development only, destructive) bun run db push ``` Migration files are generated in [`src/server/db/migrations/`](https://github.com/QuickDapp/QuickDapp/blob/v3.11.3/src/server/db/migrations/). Never edit them after creation—instead, create a new migration for fixes. ## Query Modules Database operations are organized into modules under [`src/server/db/`](https://github.com/QuickDapp/QuickDapp/blob/v3.11.3/src/server/db/): - [`users.ts`](https://github.com/QuickDapp/QuickDapp/blob/v3.11.3/src/server/db/users.ts) — User CRUD, finding by ID or creating new users - [`userAuth.ts`](https://github.com/QuickDapp/QuickDapp/blob/v3.11.3/src/server/db/userAuth.ts) — Auth method management, finding users by auth identifier - [`notifications.ts`](https://github.com/QuickDapp/QuickDapp/blob/v3.11.3/src/server/db/notifications.ts) — Creating and querying notifications, marking as read - [`worker.ts`](https://github.com/QuickDapp/QuickDapp/blob/v3.11.3/src/server/db/worker.ts) — Job scheduling, status updates, cleanup - [`settings.ts`](https://github.com/QuickDapp/QuickDapp/blob/v3.11.3/src/server/db/settings.ts) — Application settings key-value storage Each module exports functions that take `ServerApp` (or a transaction) and return typed results. ## Performance Patterns All database operations are wrapped with `startSpan` for Sentry tracing. This provides visibility into query performance and helps identify slow queries in production. For complex queries involving multiple tables, use joins rather than separate queries to avoid N+1 problems. GraphQL resolvers should never use field resolvers that trigger additional database calls—fetch all needed data in the parent resolver instead. --- # Authentication QuickDapp uses stateless JWT authentication on the backend. This page covers the server-side implementation: how tokens work, how the `@auth` directive protects operations, and how to add new authentication methods. For user-facing authentication flows (email, OAuth), see [Users > Authentication](../users/authentication). ## JWT Implementation Tokens are signed with HS256 using the `SESSION_ENCRYPTION_KEY` environment variable. The auth service in [`src/server/auth/index.ts`](https://github.com/QuickDapp/QuickDapp/blob/v3.11.3/src/server/auth/index.ts) provides three core functions: - `generateToken(payload)` — Creates a signed JWT with a 24-hour expiration - `verifyToken(token)` — Validates signature and expiration, returns the payload - `extractBearerToken(header)` — Extracts the token from an `Authorization: Bearer ...` header The token payload includes: ```typescript { type: "auth", // Token type identifier userId: number, // Database user ID iat: number, // Issued-at timestamp (seconds) iatMs: number, // Issued-at timestamp (milliseconds) jti: string // Unique token ID } ``` ## The @auth Directive GraphQL operations marked with `@auth` require a valid JWT in the Authorization header. The GraphQL handler extracts auth requirements at startup by parsing the schema and checks them before running resolvers. The validation pipeline runs in order: 1. **Extract** — Pull the Bearer token from the Authorization header 2. **Verify** — Check the JWT signature and expiration 3. **Load user** — Fetch the user record from the database by ID 4. **Check disabled** — Verify the user's `disabled` flag is false 5. **Attach to context** — Make the user available to resolvers When an unauthenticated request tries to access a protected operation, it returns a GraphQL error with `extensions.code = "UNAUTHORIZED"`. Mixed queries containing both public and protected fields fail entirely when unauthenticated—no partial data is returned. ## Error Codes The authentication system uses specific error codes defined in [`src/shared/graphql/errors.ts`](https://github.com/QuickDapp/QuickDapp/blob/v3.11.3/src/shared/graphql/errors.ts): | Code | When | |------|------| | `UNAUTHORIZED` | No token provided, or token is invalid/expired | | `AUTHENTICATION_FAILED` | Credentials are incorrect (wrong code, bad signature) | | `ACCOUNT_DISABLED` | Token is valid but the user account is disabled | ## Adding a New Authentication Method To add a custom authentication method (e.g. phone number, passkey): **1. Add the auth type constant** in `src/shared/constants.ts`: ```typescript export const AUTH_METHOD = { EMAIL: "email", PHONE: "phone", // new // ... OAuth providers } as const ``` **2. Create user lookup/creation** in `src/server/db/users.ts`: ```typescript export async function findOrCreateUserByPhone( db: Database, phoneNumber: string, ) { return withTransaction(db, async (tx) => { const existing = await tx.select() .from(userAuth) .where(and( eq(userAuth.authType, AUTH_METHOD.PHONE), eq(userAuth.authIdentifier, phoneNumber), )) .then(rows => rows[0]) if (existing) { return tx.select().from(users) .where(eq(users.id, existing.userId)) .then(rows => rows[0]) } const [user] = await tx.insert(users).values({}).returning() await tx.insert(userAuth).values({ userId: user.id, authType: AUTH_METHOD.PHONE, authIdentifier: phoneNumber, }) return user }) } ``` **3. Add the GraphQL mutation** in `src/shared/graphql/schema.ts`: ```graphql type Mutation { authenticateWithPhone(phone: String!, code: String!): AuthResult! } ``` **4. Implement the resolver** in `src/server/graphql/resolvers.ts`: ```typescript authenticateWithPhone: async (_, { phone, code }, context) => { // Verify the code, find or create user, generate JWT const user = await findOrCreateUserByPhone(context.serverApp.db, phone) const token = await generateToken({ userId: user.id }) return { success: true, token } } ``` **5. Update the frontend** to call the new mutation from a login form. ## Security **Encryption Key**: The `SESSION_ENCRYPTION_KEY` must be at least 32 characters and kept secret. It signs JWTs and encrypts OAuth state. The server validates this on startup. **HTTPS**: Always use HTTPS in production. Tokens sent over HTTP can be intercepted. **Token Storage**: The frontend stores tokens in localStorage. For higher security requirements, consider httpOnly cookies with CSRF protection. See [`src/server/auth/`](https://github.com/QuickDapp/QuickDapp/blob/v3.11.3/src/server/auth/) for the complete authentication implementation. --- # WebSockets QuickDapp uses WebSockets for real-time communication between the server and connected clients. When the application creates a notification, it gets saved to the database and immediately pushed to the user's browser sessions. ## How It Works Clients connect to `/ws` after authenticating. They send a registration message with their JWT token, and the server associates that connection with their user ID. From then on, any notification created for that user gets delivered instantly. The [`SocketManager`](https://github.com/QuickDapp/QuickDapp/blob/v3.11.3/src/server/ws/index.ts) tracks two mappings: client IDs to WebSocket connections, and user IDs to sets of client IDs. A single user can have multiple browser tabs open, and each receives the same notifications. When a worker process creates a notification, it sends an IPC message to the main server process, which then routes the message through the `SocketManager` to the user's connected clients. ## Connection Lifecycle 1. Client establishes WebSocket connection to `/ws` 2. Server sends a `Connected` message confirming the connection 3. Client sends `register` with JWT token 4. Server validates the token and associates the connection with the user 5. Server sends `Registered` confirmation 6. Server pushes `NotificationReceived` messages as notifications are created 7. On disconnect, server removes the client from its tracking maps ## Message Types Messages are defined in [`src/shared/websocket/types.ts`](https://github.com/QuickDapp/QuickDapp/blob/v3.11.3/src/shared/websocket/types.ts): - `Connected` — Initial connection acknowledgment - `Registered` — User registration successful - `NotificationReceived` — New notification with id, userId, data, createdAt, read - `Error` — Connection errors (limit exceeded, invalid token) ## Connection Limits The server enforces connection limits through configuration: - `SOCKET_MAX_TOTAL_CONNECTIONS` — Global limit across all users - `SOCKET_MAX_CONNECTIONS_PER_USER` — Per-user limit When limits are exceeded, the connection receives an `Error` message and closes. ## Client Usage The frontend's [`SocketContext`](https://github.com/QuickDapp/QuickDapp/blob/v3.11.3/src/client/contexts/SocketContext.tsx) handles the connection automatically. It reconnects when authentication state changes and provides a `subscribe()` method for listening to specific message types: ```typescript const { subscribe } = useSocket() useEffect(() => { return subscribe(WebSocketMessageType.NotificationReceived, (message) => { // Handle new notification }) }, [subscribe]) ``` There are no GraphQL subscriptions—all real-time updates flow through this WebSocket connection. See [`src/server/ws/index.ts`](https://github.com/QuickDapp/QuickDapp/blob/v3.11.3/src/server/ws/index.ts) for the server implementation and [`src/client/lib/socket.ts`](https://github.com/QuickDapp/QuickDapp/blob/v3.11.3/src/client/lib/socket.ts) for the client wrapper. --- # Mailgun QuickDapp uses Mailgun for transactional email delivery. The [`Mailer`](https://github.com/QuickDapp/QuickDapp/blob/v3.11.3/src/server/lib/mailer.ts) class wraps the Mailgun API and provides graceful fallback when not configured—emails are logged to the console instead of being sent. ## Configuration | Variable | Required | Description | |----------|----------|-------------| | `MAILGUN_API_KEY` | No | Your Mailgun API key | | `MAILGUN_API_ENDPOINT` | No | Mailgun API endpoint (defaults to US region) | | `MAILGUN_FROM_ADDRESS` | No | Sender email address (e.g., `noreply@yourdomain.com`) | The Mailer extracts the domain from `MAILGUN_FROM_ADDRESS` automatically. For EU region, set `MAILGUN_API_ENDPOINT` to `https://api.eu.mailgun.net`. ## Usage Create a Mailer instance with a logger and call `send()`: ```typescript import { Mailer } from "../lib/mailer" const mailer = new Mailer(logger) await mailer.send({ to: "user@example.com", subject: "Welcome!", text: "Thanks for signing up.", html: "

Thanks for signing up.

", }) ``` The `to` field accepts a single email address or an array for multiple recipients. Either `text` or `html` content is required—if only `text` is provided, it's used for both. ## Development Mode When Mailgun isn't configured (no `MAILGUN_API_KEY`), the Mailer logs email content to the console instead of sending: ``` [WARN] Mail client not configured - logging email to console instead: [WARN] ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ [WARN] To: user@example.com [WARN] Subject: Your verification code [WARN] Body: Your verification code is: 123456 [WARN] ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ ``` This allows development and testing without a Mailgun account. The email content appears in your terminal, making it easy to verify what would be sent. ## Email Verification Flow QuickDapp uses email verification for passwordless authentication. The flow works as follows: 1. User submits their email address 2. Server generates a verification code and encrypted blob 3. Server sends the code via Mailer 4. User enters the received code 5. Server verifies the code against the blob and creates a session ```typescript // From resolvers.ts - sending verification code const { code, blob } = await generateVerificationCodeAndBlob(logger, email) const mailer = new Mailer(logger) await mailer.send({ to: email, subject: "Your verification code", text: `Your verification code is: ${code}`, html: `

Your verification code is: ${code}

`, }) ``` The blob contains the encrypted code with an expiration timestamp, allowing stateless verification without storing codes in the database. ## Error Handling The Mailer throws errors when email delivery fails. Wrap calls in try/catch to handle failures gracefully: ```typescript try { await mailer.send({ to, subject, text }) } catch (error) { logger.error("Failed to send email:", error) // Handle failure (retry, notify user, etc.) } ``` Mailgun API errors (invalid API key, domain not verified) propagate as exceptions with descriptive messages. ## Testing In tests, Mailgun variables are left empty in `.env.test`, so emails are logged instead of sent. E2E tests can parse the logged output to extract verification codes: ```typescript // From tests/e2e/helpers/email-code.ts // In dev/test mode, the mailer logs: "Body: Your verification code is: XXXXXX" ``` See [`src/server/lib/mailer.ts`](https://github.com/QuickDapp/QuickDapp/blob/v3.11.3/src/server/lib/mailer.ts) for the implementation. --- # Logging QuickDapp uses a structured logging system built on [`@hiddentao/logger`](https://github.com/hiddentao/logger) with console and Sentry transports. Every part of the application—resolvers, workers, database operations—logs through categorized logger instances. ## Architecture The logging system has three layers: 1. **Root logger** — Created at startup with a base category (e.g. "server" or "worker") 2. **Child loggers** — Created via `serverApp.createLogger(category)` for specific subsystems 3. **Transports** — Console output with timestamps, and optionally Sentry for error capture ## Log Categories Predefined categories keep logs organized and filterable: | Category | Used By | |----------|---------| | `auth` | Authentication operations | | `graphql` | GraphQL handler lifecycle | | `graphql-resolvers` | Individual resolver execution | | `database` | Database connection and query management | | `worker-manager` | Worker process spawning and IPC | | `worker` | Individual worker job execution | Categories are defined in [`src/server/lib/logger.ts`](https://github.com/QuickDapp/QuickDapp/blob/v3.11.3/src/server/lib/logger.ts) as `LOG_CATEGORIES`. ## Usage Create a logger from the ServerApp: ```typescript const log = serverApp.createLogger("my-service") log.info("Processing request", { userId: 42 }) log.debug("Detailed data", { payload }) log.error("Operation failed", error) ``` In worker jobs, the logger is provided via the job parameters: ```typescript export const myJob: Job = { async run({ log, job }) { log.info("Starting job", { jobId: job.id }) // ... log.info("Job complete") } } ``` ## Log Levels Five levels are available, from most to least verbose: | Level | Use | |-------|-----| | `debug` | Detailed diagnostic information | | `info` | Normal operational messages | | `warn` | Unexpected but recoverable situations | | `error` | Failures that need attention | | `fatal` | Critical errors that may crash the process | Set the minimum level via environment variables: ```bash LOG_LEVEL=info # Server process log level WORKER_LOG_LEVEL=info # Worker process log level ``` ## Transports ### Console Transport Always enabled. Outputs log messages with ISO timestamps and category prefixes: ``` 2026-01-29T05:08:33.934Z [info] 🚀 QuickDapp server started 2026-01-29T05:08:34.123Z [info] Processing query: getMyNotifications ``` ### Sentry Transport Enabled when `SENTRY_DSN` is configured. Routes `error` and `fatal` level messages to Sentry for centralized error tracking. The transport attaches: - Log category as context - Any metadata passed to the log call - Stack traces for error objects ## Performance Spans The `startSpan()` function integrates with Sentry's performance monitoring to trace operation duration: ```typescript const result = await serverApp.startSpan("db.getNotifications", async () => { return await getNotifications(db, userId, pageParam) }) ``` Spans are used throughout the codebase for: - Database operations - GraphQL resolver execution - External API calls This provides visibility into where time is spent during request processing. ## Configuration ```bash LOG_LEVEL=info # trace|debug|info|warn|error WORKER_LOG_LEVEL=info # Same options, for worker processes SENTRY_DSN= # Sentry DSN for error tracking SENTRY_TRACES_SAMPLE_RATE=1.0 # Percentage of requests to trace SENTRY_PROFILE_SESSION_SAMPLE_RATE=1.0 # Percentage of sessions to profile ``` See [`src/server/lib/logger.ts`](https://github.com/QuickDapp/QuickDapp/blob/v3.11.3/src/server/lib/logger.ts) for the logger implementation and [`src/server/lib/sentry.ts`](https://github.com/QuickDapp/QuickDapp/blob/v3.11.3/src/server/lib/sentry.ts) for the Sentry transport. --- # Backend The QuickDapp backend runs on Bun with ElysiaJS as the web framework. Everything flows through the ServerApp pattern, which provides clean access to the database, logger, WebSocket manager, and worker system. ## Core Technologies The backend uses Bun as its runtime and package manager. ElysiaJS handles HTTP and WebSocket connections, GraphQL Yoga provides the API layer, and DrizzleORM manages PostgreSQL access with full TypeScript integration. ## The ServerApp Pattern Every part of the backend receives a `ServerApp` object containing all the services it needs: ```typescript type ServerApp = { app: Elysia // HTTP/WebSocket server db: Database // DrizzleORM connection rootLogger: Logger // Root logger createLogger: (category: string) => Logger // Logger factory startSpan: typeof startSpan // Sentry performance tracing workerManager: WorkerManager // Background job manager socketManager: ISocketManager // WebSocket manager createNotification: (userId, data) => Promise // Send user notifications } ``` GraphQL resolvers receive this through their context. Worker jobs get it as their first parameter. Any service you build can accept `ServerApp` to access shared resources. :::note Variants may extend the `ServerApp` type with additional fields. For example, the [Web3 variant](../variants/web3) adds `publicClient` and `walletClient` for blockchain access. ::: ## Directory Structure ``` src/server/ ├── auth/ # Authentication (email, OAuth) ├── bootstrap.ts # ServerApp creation ├── db/ # Schema, queries, connection management ├── graphql/ # Resolvers and schema integration ├── lib/ # Logger, errors, crypto ├── services/ # Business logic services ├── start-server.ts # Server startup ├── start-worker.ts # Worker process startup ├── types.ts # ServerApp type definition ├── workers/ # Background job system └── ws/ # WebSocket implementation ``` ## How Requests Flow A GraphQL request arrives at ElysiaJS, which passes it to GraphQL Yoga. Yoga parses the query, checks for the `@auth` directive, and builds a context containing the `ServerApp` and authenticated user (if any). The resolver runs, queries the database through DrizzleORM, and returns the result. WebSocket connections follow a similar pattern. Clients connect, optionally authenticate with a JWT, and receive real-time updates when notifications are created. The [`SocketManager`](https://github.com/QuickDapp/QuickDapp/blob/v3.11.3/src/server/ws/index.ts) routes messages to specific users or broadcasts to everyone. Background jobs get submitted to the database queue. Worker processes poll for pending jobs, execute them with full `ServerApp` access, and mark them complete. Workers can send WebSocket messages back through IPC to the main server process. See [`src/server/types.ts`](https://github.com/QuickDapp/QuickDapp/blob/v3.11.3/src/server/types.ts) for the complete `ServerApp` type definition. --- # LLM Plugin The QuickDapp LLM plugin gives your AI code editor deep knowledge of QuickDapp conventions, so it can scaffold projects, generate code that follows best practices, and safely update your `CLAUDE.md` without clobbering QuickDapp-specific instructions. ## Installation (Claude Code) Add the QuickDapp marketplace to Claude Code: ``` /plugin marketplace add QuickDapp/llm-plugins ``` Install the plugin: ``` /plugin install quickdapp@quickdapp-plugins ``` ## Commands ### create-project Scaffold a new QuickDapp project interactively. ``` /quickdapp:create-project ``` Prompts for folder name, variant (base or web3), and package runner (bunx or npx), then runs the CLI and displays next steps. ## Skills ### write-code Automatically activated when writing code in a QuickDapp project. Ensures adherence to project conventions by: - Reading the project `CLAUDE.md` for conventions - Fetching version-specific documentation from quickdapp.xyz - Enforcing TypeScript, DrizzleORM, GraphQL schema-first, React named exports, and other conventions - Detecting the Web3 variant and applying additional conventions - Flagging convention violations before proceeding ### update-claude-md Activated when modifying a QuickDapp project's `CLAUDE.md`. Protects QuickDapp-specific instructions by: - Identifying and preserving QuickDapp sections (Overview, Documentation, CLI Commands, Coding Guidelines, Web3) - Adding user instructions in clearly separated sections - Detecting conflicts between user requests and QuickDapp conventions - Asking the user which should take precedence on conflict --- # Vibe coding QuickDapp is probably one of the first web boilerplates that's built with vibe coding in mind (Of course, you can still code by hand if you wish!). It ships with a [`CLAUDE.md`](https://github.com/QuickDapp/QuickDapp/blob/v3.11.3/packages/base/CLAUDE.md) file already present. This file contains instructions for your vibe coding AI to help it build your app _the QuickDapp way_. _Note: Feel free to update `CLAUDE.md` with statements specific to your web app, but we recommend leaving the QuickDapp-specific instructions intact._ For an even better experience, install the [QuickDapp LLM plugin](./llm-plugins) for your AI code editor. It provides project scaffolding commands, convention-aware code generation, and safe CLAUDE.md editing. To get a sense of how easy it is to vibe code with QuickDapp check out one of the [tutorials](../tutorials). --- # Tutorial: Chatroom Todo --- # Tutorials This section contains tutorials for building and deploying a QuickDapp app, from start to finish. All the tutorials assume vibe coding with an AI-powered code editor. * [Build a chatroom](./chatroom) --- # GraphQL Client The frontend communicates with the backend API using `graphql-request` paired with React Query for caching and state management. ## Client Setup The [`getGraphQLClient()`](https://github.com/QuickDapp/QuickDapp/blob/v3.11.3/src/shared/graphql/client.ts) function returns a singleton GraphQL client configured with the API endpoint. The [`setAuthToken()`](https://github.com/QuickDapp/QuickDapp/blob/v3.11.3/src/shared/graphql/client.ts) function updates the Authorization header when a user signs in or out. The [`AuthContext`](https://github.com/QuickDapp/QuickDapp/blob/v3.11.3/src/client/contexts/AuthContext.tsx) manages the token lifecycle. When authentication succeeds, it stores the JWT and configures the client. On sign-out or token expiry, it clears the header and resets the auth state. ## Operations Queries and mutations are defined in the shared folder so both client and server can reference them. The main operations include: **Authentication**: [`VALIDATE_TOKEN`](https://github.com/QuickDapp/QuickDapp/blob/v3.11.3/src/shared/graphql/queries.ts) checks if the current JWT is valid on app initialization. The email and OAuth mutations handle sign-in flows. **Notifications**: [`GET_MY_NOTIFICATIONS`](https://github.com/QuickDapp/QuickDapp/blob/v3.11.3/src/shared/graphql/queries.ts) fetches paginated notifications, [`GET_MY_UNREAD_NOTIFICATIONS_COUNT`](https://github.com/QuickDapp/QuickDapp/blob/v3.11.3/src/shared/graphql/queries.ts) returns the badge count, and mutations mark notifications as read. See [`src/shared/graphql/queries.ts`](https://github.com/QuickDapp/QuickDapp/blob/v3.11.3/src/shared/graphql/queries.ts) and [`src/shared/graphql/mutations.ts`](https://github.com/QuickDapp/QuickDapp/blob/v3.11.3/src/shared/graphql/mutations.ts) for all available operations. ## Real-time Updates GraphQL doesn't handle real-time updates. Instead, WebSockets push notifications directly to connected clients. When the server creates a notification, it emits a `NotificationReceived` message through the socket connection. The [`useNotifications`](https://github.com/QuickDapp/QuickDapp/blob/v3.11.3/src/client/hooks/useNotifications.ts) hook listens for these messages and updates the React Query cache automatically. ## Error Handling When the server returns an `UNAUTHORIZED` error code, the [`AuthContext`](https://github.com/QuickDapp/QuickDapp/blob/v3.11.3/src/client/contexts/AuthContext.tsx) clears the token and prompts re-authentication. Other GraphQL errors surface through React Query's error handling. --- # Theming QuickDapp includes a complete theming system with dark and light mode support, system preference detection, and CSS integration through Tailwind v4. ## ThemeContext The [`ThemeProvider`](https://github.com/QuickDapp/QuickDapp/blob/v3.11.3/src/client/contexts/ThemeContext.tsx) manages theme state throughout the application. It supports three preferences: - **system** — Follows the operating system's dark/light setting - **light** — Forces light mode - **dark** — Forces dark mode ```typescript const { preference, resolvedTheme, setPreference } = useTheme() // preference: "system" | "light" | "dark" // resolvedTheme: "light" | "dark" (what's actually shown) ``` ## System Theme Detection When preference is set to "system", the provider uses `window.matchMedia("(prefers-color-scheme: dark)")` to detect the OS setting. It also listens for changes, so switching your OS theme updates the app in real-time. ## Persistence The selected preference is saved to localStorage under the `"theme"` key. On page load, the provider reads this value and applies it immediately, preventing a flash of the wrong theme. ## ThemeSwitcher Component The [`ThemeSwitcher`](https://github.com/QuickDapp/QuickDapp/blob/v3.11.3/src/client/components/ThemeSwitcher.tsx) provides a popover UI with system, light, and dark options. It's included in the header by default. ## CSS Integration The theme works by adding `"light"` or `"dark"` to the `` element's class list. Tailwind v4's `@theme` directive in [`globals.css`](https://github.com/QuickDapp/QuickDapp/blob/v3.11.3/src/client/styles/globals.css) defines the color palette: ```css @theme { --color-anchor: #0ec8ff; --color-background: #ffffff; --color-foreground: #374151; --color-muted: #9ca3af; --color-border: #e5e7eb; } html.dark { --color-anchor: #0ec8ff; --color-background: #0f172a; --color-foreground: #f8fafc; --color-muted: #94a3b8; --color-border: #334155; } ``` These CSS variables are referenced by Tailwind utility classes (`bg-background`, `text-foreground`, `text-anchor`, etc.) and automatically switch when the theme changes. ## Custom Colors and Utilities The theme defines several custom utilities in `globals.css`: | Utility | Description | |---------|-------------| | `btn-primary` | Primary action button styling | | `btn-secondary` | Secondary action button styling | | `card` | Card container with border and shadow | | `glow-effect` | Subtle glow shadow using the anchor color | | `glow-strong` | Stronger glow effect | | `flex-center` | Centered flex container | | `flex-between` | Space-between flex container | | `gradient-bg` | Fixed gradient background | ## The cn() Utility The [`cn()`](https://github.com/QuickDapp/QuickDapp/blob/v3.11.3/src/client/utils/cn.ts) helper combines `clsx` and `tailwind-merge` for conditional class application: ```typescript import { cn } from "../utils/cn"
``` `tailwind-merge` handles deduplication, so conflicting classes resolve correctly (e.g. `"p-4 p-2"` becomes `"p-2"`). ## Adding Custom Theme Colors To add a new theme-aware color: 1. Add the light value in the `@theme` block 2. Add the dark value in the `html.dark` block 3. Use it with Tailwind: `bg-[var(--color-mycolor)]` or define it as a named color in `@theme` See [`src/client/contexts/ThemeContext.tsx`](https://github.com/QuickDapp/QuickDapp/blob/v3.11.3/src/client/contexts/ThemeContext.tsx) for the provider implementation and [`src/client/styles/globals.css`](https://github.com/QuickDapp/QuickDapp/blob/v3.11.3/src/client/styles/globals.css) for the complete theme definition. --- # Static Assets QuickDapp supports two types of static assets: files processed by Vite (images imported in React components) and passthrough files served directly (favicon, robots.txt, custom fonts). ## Where to Put Static Files Static files that should be served directly without Vite processing go in `src/server/static-src/`. This is the source directory for assets like: - `favicon.ico` — Site favicon - `robots.txt` — Search engine instructions - Custom fonts or other files that need direct URL access ``` src/server/ ├── static-src/ # YOUR static files go here │ ├── favicon.ico # Site favicon │ └── robots.txt # Search engine config └── static/ # Generated - don't edit directly ``` ## How It Works Vite's `publicDir` is configured to point to `src/server/static-src/`: ```typescript // From src/client/vite.config.ts publicDir: path.resolve(__dirname, "../server/static-src"), ``` This means: - **Development**: Vite serves files from `static-src/` directly at the root URL - **Production build**: Vite copies `static-src/` contents to the build output alongside the compiled frontend The build system also copies `static-src/` to `src/server/static/` via [`scripts/shared/copy-static-src.ts`](https://github.com/QuickDapp/QuickDapp/blob/v3.11.3/scripts/shared/copy-static-src.ts) for the server to serve in production. The server uses ElysiaJS's static plugin to serve files from the `static/` directory: ```typescript // From src/server/start-server.ts const staticDir = serverConfig.STATIC_ASSETS_FOLDER || path.join(import.meta.dir, "static") app.use(staticPlugin({ assets: staticDir, prefix: "", indexHTML: true, alwaysStatic: true, })) ``` The `indexHTML: true` option enables SPA routing—unmatched routes serve `index.html` so client-side routing works. ## Static vs Vite-Processed Assets **Use `static-src/`** for files that need a predictable URL: - Favicon, robots.txt, sitemap.xml - Files referenced in `` meta tags - Assets loaded by external services **Use Vite imports** for assets used in React components: ```tsx // Vite handles optimization, hashing, and bundling import logo from './images/logo.png' function Header() { return Logo } ``` Vite-imported assets get content hashing (`logo.abc123.png`) for cache busting. Files in `static-src/` keep their original names. ## Adding Custom Static Files To add a static file: 1. Create the file in `src/server/static-src/` 2. Access it immediately at the root URL during dev (e.g., `/robots.txt`) 3. Run `bun run build` for production Example `robots.txt`: ``` User-agent: * Allow: / Sitemap: https://example.com/sitemap.xml ``` ## Important Notes - **All runtime static assets go in `src/server/static-src/`** — this includes manually created files and script-generated files - **Never edit `src/server/static/` directly** — it's auto-populated from `static-src/` on every build - **Binary deployments** extract static files to a temp directory at runtime; the `STATIC_ASSETS_FOLDER` environment variable points to this location --- # Forms QuickDapp uses a custom hook-based form system rather than external libraries like React Hook Form. The [`useForm`](https://github.com/QuickDapp/QuickDapp/blob/v3.11.3/src/client/hooks/useForm.ts) and [`useField`](https://github.com/QuickDapp/QuickDapp/blob/v3.11.3/src/client/hooks/useForm.ts) hooks provide validation, error handling, and field state management. ## The useField Hook Each form field gets its own `useField` instance that tracks value, validation state, and touched status: ```typescript const nameField = useField({ initialValue: "", validate: (value) => { if (!value.trim()) return "Name is required" if (value.length < 2) return "Name must be at least 2 characters" } }) ``` The hook returns: - `name` — Field name from options - `value` — Current field value - `valid` — Whether the field passes validation - `error` — Validation error message (if any) - `version` — Increments on each change - `isSet` — True if the field has a value or is optional - `isValidating` — True during async validation - `handleChange(value)` — Update the field value - `unset()` — Reset to initial state ## Async Validation Fields support async validation with debouncing. This is useful for checking availability or validating against a server: ```typescript const addressField = useField({ initialValue: "", validateAsync: async (value) => { const isValid = await checkAddressOnChain(value) if (!isValid) return "Invalid address" }, validateAsyncDebounceMs: 300 }) ``` The `isValidating` flag indicates when async validation is in progress, so you can show a loading indicator. ## The useForm Hook For forms with multiple fields, `useForm` coordinates validation across all fields: ```typescript const form = useForm({ onSubmit: async (values) => { await createToken(values) } }) ``` The form tracks overall validity and handles submission. Fields register themselves with the form and validation runs on submit. ## Form Components The form UI components in [`Form.tsx`](https://github.com/QuickDapp/QuickDapp/blob/v3.11.3/src/client/components/Form.tsx) integrate with the field hooks: `Input` and `Textarea` accept an `error` prop to display validation messages below the field. They show a red border when invalid. `Label` wraps Radix UI's label with optional required indicator styling. `FormField` combines a label, input, and error message into a single component. `TextInput` and `NumberInput` are pre-integrated with `useField` for common use cases. `FieldSuffix` shows a spinning indicator during async validation. ## Validation Patterns Validate on change for immediate feedback: ```typescript const field = useField({ initialValue: "", validate: (value) => { if (!value) return "Required" } }) // Error updates as user types ``` Validate on blur for less intrusive feedback: ```typescript // Check field.isSet before showing errors {field.isSet && field.error && {field.error}} ``` Combine sync and async validation: ```typescript const field = useField({ initialValue: "", validate: (value) => { // Sync validation runs first if (!isAddress(value)) return "Invalid address format" }, validateAsync: async (value) => { // Async only runs if sync passes const exists = await checkAddressExists(value) if (!exists) return "Address not found" } }) ``` ## Sanitization Fields can sanitize values before validation: ```typescript const field = useField({ initialValue: "", sanitize: (value) => value.toLowerCase().trim() }) ``` The sanitized value is what gets validated and submitted. --- # Global State QuickDapp uses React Context for global state management. The application separates concerns into focused providers: theming, authentication, WebSocket connections, and toast notifications. Each handles one responsibility and exposes a hook for components to access its data. ## Provider Structure The [`App.tsx`](https://github.com/QuickDapp/QuickDapp/blob/v3.11.3/src/client/App.tsx) nests providers to make them available throughout the application: ```tsx {/* routes */} ``` ## Theme Context The [`ThemeProvider`](https://github.com/QuickDapp/QuickDapp/blob/v3.11.3/src/client/contexts/ThemeContext.tsx) manages dark/light mode with system preference detection. ```typescript interface ThemeContextValue { preference: ThemePreference // "system" | "light" | "dark" resolvedTheme: ResolvedTheme // "light" | "dark" setPreference: (preference: ThemePreference) => void } ``` The provider resolves the "system" preference by checking `window.matchMedia("(prefers-color-scheme: dark)")` and listens for changes. The resolved theme is applied by adding `"light"` or `"dark"` to the HTML root element's class list, which activates the corresponding CSS variables. Theme preference is persisted to localStorage under the `"theme"` key. Access via `useTheme()`. See [Theming](./theming) for details on CSS integration. ## Authentication Context The [`AuthContext`](https://github.com/QuickDapp/QuickDapp/blob/v3.11.3/src/client/contexts/AuthContext.tsx) manages email and OAuth authentication. It uses a state machine to track the authentication lifecycle. ```typescript interface AuthContextValue { isAuthenticated: boolean isLoading: boolean error: Error | null authToken: string | null profile: UserProfile | null email: string | null login: (token: string, profile: UserProfile) => void logout: () => void restoreAuth: () => void } ``` On mount, the context attempts to restore a previous session by reading the JWT from localStorage and validating it with the server. If valid, it fetches the user profile via the `me` query. The context tracks several states: `IDLE`, `RESTORING` (checking for existing session), `AUTHENTICATING`, `AUTHENTICATED`, and `ERROR`. Access the context via `useAuthContext()`. ## Socket Context The [`SocketProvider`](https://github.com/QuickDapp/QuickDapp/blob/v3.11.3/src/client/contexts/SocketContext.tsx) manages WebSocket connections for real-time updates. It initializes after auth loading completes and automatically reconnects with the appropriate token when authentication state changes. ```typescript interface SocketContextValue { connected: boolean subscribe: (type: WebSocketMessageType, handler: (message: WebSocketMessage) => void) => () => void } ``` When a user authenticates, the socket reconnects with their JWT to establish an authenticated session. When they log out, it reconnects without a token. The `subscribe()` method returns an unsubscribe function for cleanup. Access the context via `useSocket()`. ## Toast Notifications The [`ToastProvider`](https://github.com/QuickDapp/QuickDapp/blob/v3.11.3/src/client/components/Toast.tsx) manages temporary notification messages displayed in the UI. Toasts have types (default, success, error, warning), optional titles and descriptions, and auto-dismiss after a configurable duration. ```typescript interface ToastContextType { toasts: Toast[] addToast: (toast: Omit) => void removeToast: (id: string) => void } ``` Toasts auto-dismiss after 5 seconds by default. The container renders in the top-right corner with slide-in animations. Access the context via `useToast()`. ## Notification Hook The [`useNotifications`](https://github.com/QuickDapp/QuickDapp/blob/v3.11.3/src/client/hooks/useNotifications.ts) hook subscribes to real-time notification events from the WebSocket connection. It wraps the socket subscription for the `NotificationReceived` message type. ```typescript useNotifications({ onNotificationReceived: (notification) => { // Handle notification } }) ``` This hook provides a cleaner API than directly subscribing to socket messages for notifications. ## Cookie Consent (Optional) The [`CookieConsentProvider`](https://github.com/QuickDapp/QuickDapp/blob/v3.11.3/src/client/contexts/CookieConsentContext.tsx) tracks whether users have accepted or declined cookies. It shows a banner component and persists the choice to localStorage. ```typescript interface CookieConsentContextValue { consent: "accepted" | "declined" | null hasConsented: boolean isAccepted: boolean isDeclined: boolean acceptCookies: () => void declineCookies: () => void resetConsent: () => void } ``` This provider is not included in the default [`App.tsx`](https://github.com/QuickDapp/QuickDapp/blob/v3.11.3/src/client/App.tsx) but can be added when needed for GDPR compliance. Access via `useCookieConsent()`. --- # Components QuickDapp includes a small set of UI components built on Radix UI primitives with TailwindCSS styling. The focus is on essential functionality rather than a comprehensive design system. ## Base Components [`Button`](https://github.com/QuickDapp/QuickDapp/blob/v3.11.3/src/client/components/Button.tsx) provides variants (default, outline, ghost, error), sizes, and loading state. It uses the `cn()` utility for class merging and forwards refs properly. [`Dialog`](https://github.com/QuickDapp/QuickDapp/blob/v3.11.3/src/client/components/Dialog.tsx) wraps Radix UI's dialog primitive with consistent styling—dark background, blur overlay, and close button. The `DialogContent`, `DialogHeader`, `DialogTitle`, and `DialogDescription` components compose together for modal interfaces. **Form components** in [`Form.tsx`](https://github.com/QuickDapp/QuickDapp/blob/v3.11.3/src/client/components/Form.tsx) include `Input`, `Textarea`, `Label`, and `FormField`. `Input` and `Textarea` display validation errors below the field. `Label` supports a required indicator. These integrate with the custom [`useForm`](https://github.com/QuickDapp/QuickDapp/blob/v3.11.3/src/client/hooks/useForm.ts) hook for validation. [`Toast`](https://github.com/QuickDapp/QuickDapp/blob/v3.11.3/src/client/components/Toast.tsx) provides temporary notification messages. The `ToastProvider` manages a list of toasts with auto-dismiss. Toast types include default, success, error, and warning with corresponding colors and icons. ## Layout The application uses a simple layout with a fixed header and main content area. The [`Header`](https://github.com/QuickDapp/QuickDapp/blob/v3.11.3/src/client/components/Header.tsx) component shows the logo and notification indicator (when authenticated). There's no sidebar—the application focuses on the main content area. Routing uses React Router with a single page currently ([`HomePage`](https://github.com/QuickDapp/QuickDapp/blob/v3.11.3/src/client/pages/HomePage.tsx)). ``` ┌─────────────────────────────────────┐ │ Header (fixed, 56px) │ ├─────────────────────────────────────┤ │ │ │ Main Content │ │ │ ├─────────────────────────────────────┤ │ Footer │ └─────────────────────────────────────┘ ``` ## Notification Components Real-time notification display: - [`NotificationsIndicator`](https://github.com/QuickDapp/QuickDapp/blob/v3.11.3/src/client/components/notifications/NotificationsIndicator.tsx) — Bell icon in header with unread count badge - [`NotificationsDialog`](https://github.com/QuickDapp/QuickDapp/blob/v3.11.3/src/client/components/notifications/NotificationsDialog.tsx) — Full list of notifications with mark-as-read functionality - [`NotificationComponents`](https://github.com/QuickDapp/QuickDapp/blob/v3.11.3/src/client/components/notifications/NotificationComponents.tsx) — Individual renderers for different notification types ## Utility Components [`Loading`](https://github.com/QuickDapp/QuickDapp/blob/v3.11.3/src/client/components/Loading.tsx) shows a spinning indicator for async operations. [`ErrorBoundary`](https://github.com/QuickDapp/QuickDapp/blob/v3.11.3/src/client/components/ErrorBoundary.tsx) catches component errors and displays a fallback UI instead of crashing the application. [`ErrorMessageBox`](https://github.com/QuickDapp/QuickDapp/blob/v3.11.3/src/client/components/ErrorMessageBox.tsx) displays styled error messages. [`Popover`](https://github.com/QuickDapp/QuickDapp/blob/v3.11.3/src/client/components/Popover.tsx) wraps Radix UI's popover for dropdown content. [`Tooltip`](https://github.com/QuickDapp/QuickDapp/blob/v3.11.3/src/client/components/Tooltip.tsx) wraps Radix UI's tooltip for hover hints. [`ThemeSwitcher`](https://github.com/QuickDapp/QuickDapp/blob/v3.11.3/src/client/components/ThemeSwitcher.tsx) provides a popover with system/light/dark theme options. See [Theming](./theming) for details. [`OnceVisibleInViewport`](https://github.com/QuickDapp/QuickDapp/blob/v3.11.3/src/client/components/OnceVisibleInViewport.tsx) renders children only when the component scrolls into view, useful for lazy loading. [`CookieConsentBanner`](https://github.com/QuickDapp/QuickDapp/blob/v3.11.3/src/client/components/CookieConsentBanner.tsx) displays a GDPR-compliant cookie consent banner when needed. ## Styling Approach Components use TailwindCSS with the [`cn()`](https://github.com/QuickDapp/QuickDapp/blob/v3.11.3/src/client/utils/cn.ts) utility. This combines `clsx` for conditional classes and `tailwind-merge` for deduplication: ```typescript import { cn } from "../utils/cn"
``` The theme is defined in [`globals.css`](https://github.com/QuickDapp/QuickDapp/blob/v3.11.3/src/client/styles/globals.css) using Tailwind v4's `@theme` directive. See [Theming](./theming) for details on colors and custom utilities. --- # Frontend The QuickDapp frontend is a React 19 application built with Vite, TypeScript, and TailwindCSS. ## Technology Stack Vite handles development and production builds with hot module replacement. TailwindCSS v4 provides styling with dark and light theme support and custom utility classes. Radix UI supplies accessible primitives for dialogs, popovers, and tooltips. For data fetching, React Query manages server state with caching and background refetching. The GraphQL client uses `graphql-request` with queries defined in the shared folder. ## Project Structure ``` src/client/ ├── App.tsx # Root with provider setup ├── components/ # UI components ├── contexts/ # ThemeContext, AuthContext, SocketContext, CookieConsentContext ├── hooks/ # useForm, useNotifications ├── lib/ # Socket client ├── pages/ # Page components ├── styles/ # Tailwind globals └── utils/ # cn() helper ``` ## Provider Structure The [`App`](https://github.com/QuickDapp/QuickDapp/blob/v3.11.3/src/client/App.tsx) component wraps content with providers: ```tsx {/* routes */} ``` ## Key Patterns **State management** uses React Context for global state (theme, auth, sockets, toasts) and React Query for server data. There's no Redux or external state library. **Forms** use a custom hook-based validation system in [`useForm.ts`](https://github.com/QuickDapp/QuickDapp/blob/v3.11.3/src/client/hooks/useForm.ts). It supports sync and async validation with debouncing, without external form libraries. **Styling** combines TailwindCSS utilities with the [`cn()`](https://github.com/QuickDapp/QuickDapp/blob/v3.11.3/src/client/utils/cn.ts) helper for conditional classes. CSS Modules handle component-specific styles where needed. **Configuration** comes from [`clientConfig`](https://github.com/QuickDapp/QuickDapp/blob/v3.11.3/src/shared/config/client.ts) in the shared folder. The build process injects environment values so they're available at runtime. --- # Notifications QuickDapp provides a real-time notification system that persists to the database and delivers instantly via WebSocket. Notifications can be created from resolvers, workers, or any code with `ServerApp` access. ## How It Works When a notification is created, two things happen simultaneously: 1. The notification is inserted into the `notifications` database table 2. A WebSocket message is sent to the user's connected browser sessions This dual approach means notifications are never lost — even if the user is offline, they'll see them when they next load the app via the database query. ## Creating Notifications Use `serverApp.createNotification()` from anywhere with `ServerApp` access: ```typescript await serverApp.createNotification(userId, { type: "order_completed", message: "Your order has been processed", orderId: "12345" }) ``` The `data` field is a JSON object — store whatever your notification type needs. The `type` field is a string you define per notification kind. ### From Resolvers ```typescript const resolvers = { Mutation: { completeOrder: async (_, { orderId }, context) => { const { serverApp, user } = context // ... process order ... await serverApp.createNotification(user.id, { type: "order_completed", message: "Your order is ready" }) } } } ``` ### From Worker Jobs Workers send notifications through IPC — the main server process handles the actual WebSocket delivery: ```typescript export const processOrderJob: Job = { async run({ serverApp, job }) { // ... process order ... await serverApp.createNotification(job.userId, { type: "order_shipped", message: "Your order has shipped" }) } } ``` ## Data Model The `notifications` table stores: | Field | Type | Description | |-------|------|-------------| | `id` | serial | Primary key | | `userId` | integer | Owner of the notification | | `data` | JSON | Notification payload (type, message, custom fields) | | `read` | boolean | Whether the user has seen it | | `createdAt` | timestamp | When it was created | ## Frontend Integration ### useNotifications Hook The [`useNotifications`](https://github.com/QuickDapp/QuickDapp/blob/v3.11.3/src/client/hooks/useNotifications.ts) hook subscribes to real-time WebSocket events: ```typescript useNotifications({ onNotificationReceived: (notification) => { // Update React Query cache, show toast, etc. } }) ``` ### NotificationsIndicator The [`NotificationsIndicator`](https://github.com/QuickDapp/QuickDapp/blob/v3.11.3/src/client/components/notifications/NotificationsIndicator.tsx) shows a bell icon in the header. When unread notifications exist, it displays a badge with the count. Clicking it opens the notifications dialog. The unread count is fetched via the `getMyUnreadNotificationsCount` GraphQL query and updated in real-time when new notifications arrive via WebSocket. ### NotificationsDialog The [`NotificationsDialog`](https://github.com/QuickDapp/QuickDapp/blob/v3.11.3/src/client/components/notifications/NotificationsDialog.tsx) displays a paginated list of notifications with: - Infinite scroll for loading older notifications - "Mark as read" for individual notifications - "Mark all as read" button - React Query cache integration for instant UI updates ### NotificationComponents The [`NotificationComponents`](https://github.com/QuickDapp/QuickDapp/blob/v3.11.3/src/client/components/notifications/NotificationComponents.tsx) file contains renderers for different notification types. Each notification type gets its own display component based on the `data.type` field. ## Custom Notification Types To add a new notification type: 1. **Define the type constant** and the data shape for your notification 2. **Create notifications** using `serverApp.createNotification()` with your type 3. **Add a renderer** in `NotificationComponents.tsx` to display the notification's content ## GraphQL Operations Two queries and two mutations handle notification management: - `getMyNotifications(pageParam)` — Paginated fetch with `startIndex` and `perPage` - `getMyUnreadNotificationsCount` — Count for the badge - `markNotificationAsRead(id)` — Mark a single notification as read - `markAllNotificationsAsRead` — Mark all notifications as read All notification operations require authentication via the `@auth` directive. --- # Authentication QuickDapp uses stateless JWT authentication with two provider options: email verification and OAuth. This page covers the authentication flows from the user's perspective. For backend implementation details (JWT internals, the `@auth` directive), see [Backend > Authentication](../backend/authentication). ## JWT Tokens All authentication methods produce a JWT token signed with `SESSION_ENCRYPTION_KEY`. The token contains the user ID and expires after 24 hours. There's no session table—authentication state lives entirely in the token. ## Email Authentication Email verification uses encrypted, stateless codes: 1. Client calls `sendEmailVerificationCode` with email address 2. Server generates a random code and encrypts it into a blob containing the code and expiration 3. Server sends the code via email 4. User enters the code they received 5. Client calls `authenticateWithEmail` with email, code, and blob 6. Server decrypts the blob, verifies the code matches and hasn't expired 7. Server finds or creates the user and returns a JWT The blob approach means no database storage for pending verifications. Codes expire after a short window. Email configuration requires `MAILGUN_*` environment variables for the email provider. ## OAuth Authentication Six OAuth providers are supported: Google, Facebook, GitHub, X (Twitter), TikTok, and LinkedIn. The flow: 1. Client calls `getOAuthLoginUrl` with provider name and redirect URL 2. Server generates encrypted state containing PKCE challenge and redirect info 3. User is redirected to the OAuth provider's authorization page 4. After authorization, provider redirects to `/auth/callback/:provider` 5. Server exchanges the authorization code for tokens 6. Server fetches user info from the provider's API 7. Server finds or creates the user based on provider user ID 8. Server returns an HTML page that stores the JWT and redirects to the app Each provider requires configuration: ```bash OAUTH_GOOGLE_CLIENT_ID=... OAUTH_GOOGLE_CLIENT_SECRET=... OAUTH_GITHUB_CLIENT_ID=... OAUTH_GITHUB_CLIENT_SECRET=... # Similar for Facebook, X, TikTok, LinkedIn ``` The OAuth implementation uses the Arctic library and handles PKCE automatically for providers that support it. ## Adding Custom Authentication Methods To add a new authentication method, follow these steps: 1. **Add auth type constant** — Define the new type in `src/shared/constants.ts` 2. **Create user lookup/creation** — Add a function in `src/server/db/users.ts` to find or create users by the new identifier 3. **Add authentication logic** — Implement verification in a new auth service method 4. **Create GraphQL mutation** — Add the mutation to the schema and implement the resolver 5. **Update frontend** — Create a login form that calls the new mutation See [Backend > Authentication](../backend/authentication) for detailed implementation guidance. ## Frontend Integration The [`AuthContext`](https://github.com/QuickDapp/QuickDapp/blob/v3.11.3/src/client/contexts/AuthContext.tsx) exposes: - `isAuthenticated` — Whether the user is logged in - `isLoading` — Loading state during auth operations - `error` — Error from failed auth attempts - `authToken` — The current JWT (null if not authenticated) - `profile` — The authenticated user's profile - `email` — The authenticated user's email - `login(token, profile)` — Store auth state after successful authentication - `logout()` — Clear auth state - `restoreAuth()` — Attempt to restore auth from stored token on app load ## Token Validation Protected GraphQL operations use the `@auth` directive. The handler extracts the Bearer token, verifies it, loads the user, and checks the `disabled` flag. Invalid tokens return `UNAUTHORIZED`, disabled users return `ACCOUNT_DISABLED`. See [Backend > Authentication](../backend/authentication) for details. ## Security **Encryption Key**: The `SESSION_ENCRYPTION_KEY` must be 32+ characters and kept secret. It signs JWTs and encrypts OAuth state. **HTTPS**: Always use HTTPS in production. Tokens sent over HTTP can be intercepted. **Token Storage**: The frontend stores tokens in localStorage. For higher security requirements, consider httpOnly cookies with CSRF protection. See [`src/server/auth/`](https://github.com/QuickDapp/QuickDapp/blob/v3.11.3/src/server/auth/) for the complete authentication implementation. --- # Users QuickDapp provides a simple user management system with flexible authentication. Users can sign in via email verification or OAuth providers. Each authentication method links to a single user record, allowing multiple sign-in options per account. ## User Model The user system uses two tables: `users` for account records and `userAuth` for authentication methods. The [`users`](https://github.com/QuickDapp/QuickDapp/blob/v3.11.3/src/server/db/schema.ts) table stores minimal account data: ```typescript export const users = pgTable("users", { id: serial("id").primaryKey(), disabled: boolean("disabled").default(false).notNull(), settings: json("settings"), createdAt: timestamp("created_at").defaultNow().notNull(), updatedAt: timestamp("updated_at").defaultNow().notNull(), }) ``` The [`userAuth`](https://github.com/QuickDapp/QuickDapp/blob/v3.11.3/src/server/db/schema.ts) table links authentication methods to users: ```typescript export const userAuth = pgTable("user_auth", { id: serial("id").primaryKey(), userId: integer("user_id").references(() => users.id).notNull(), authType: text("auth_type").notNull(), // "email", "google", etc. authIdentifier: text("auth_identifier").unique().notNull(), // email, provider ID, etc. createdAt: timestamp("created_at").defaultNow().notNull(), updatedAt: timestamp("updated_at").defaultNow().notNull(), }) ``` This separation allows a single user to have multiple authentication methods—for example, an email address and a Google account—all pointing to the same user record. :::note Variants can add additional authentication types. For example, the [Web3 variant](../variants/web3/authentication) adds wallet-based authentication via SIWE (Sign-In With Ethereum). ::: ## Authentication Methods QuickDapp supports two base authentication approaches: **Email Verification** — Users receive a verification code via email. The code is encrypted into a stateless blob, avoiding database storage for pending verifications. **OAuth** — Six providers are supported: Google, Facebook, GitHub, X (Twitter), TikTok, and LinkedIn. Each uses encrypted state for CSRF protection. All methods result in a JWT token stored in the client and sent with subsequent API requests. ## User Lifecycle Users are created automatically on first authentication. The flow: 1. User authenticates via their chosen method 2. Server looks up `userAuth` by auth identifier 3. If found, retrieve the linked user 4. If not found, create a new user and auth record 5. Return JWT token This just-in-time creation means there's no separate registration step. Users exist as soon as they authenticate. ## Account Linking A user can add additional authentication methods to their account. For example, someone who signed up with email can later add Google OAuth. The new auth record links to the existing user ID. The unique constraint on `authIdentifier` prevents the same email or provider account from linking to multiple users. ## User Disabling Set `users.disabled = true` to prevent a user from accessing protected operations. The GraphQL handler checks this flag and returns `ACCOUNT_DISABLED` errors for disabled users. Disabling doesn't delete the user or their data—it only prevents new API access. ## User Settings The `settings` JSON field stores user preferences. There's no fixed schema—store whatever your application needs: ```typescript // Update user settings await serverApp.db .update(users) .set({ settings: { theme: "dark", notifications: true } }) .where(eq(users.id, userId)) ``` --- # Test E2E QuickDapp includes end-to-end browser testing powered by [Playwright](https://playwright.dev/). ## Running Tests ```shell bun run test:e2e # Run headless browser tests bun run test:e2e --headed # Run with visible browser window bun run test:e2e --ui # Open Playwright's interactive UI mode ``` ## What Happens The E2E test runner: 1. Starts a test database container via Docker Compose (locally) 2. Pushes the database schema with `bun run db push --force` 3. Starts the dev server as a background process 4. Runs Playwright tests against the running application 5. Reports results ## Configuration The Playwright configuration lives in [`playwright.config.ts`](https://github.com/QuickDapp/QuickDapp/blob/v3.11.3/playwright.config.ts): - **Test directory**: `./tests/e2e` - **Base URL**: `http://localhost:5173` (Vite dev server) - **Browser**: Chromium (Desktop Chrome) - **Parallel**: Disabled (tests run sequentially) - **Web server**: Automatically starts the dev server and waits for it ## Writing E2E Tests E2E tests go in the `tests/e2e/` directory: ```typescript import { test, expect } from "@playwright/test" test("homepage loads", async ({ page }) => { await page.goto("/") await expect(page).toHaveTitle(/QuickDapp/) }) test("can navigate to login", async ({ page }) => { await page.goto("/") await page.click("text=Sign In") await expect(page.locator("form")).toBeVisible() }) ``` ## CI vs Local In CI environments (`CI=true`): - The test database is provided by a service container - Tests retry failed tests up to 2 times - `--forbidOnly` prevents `.only` from passing in CI Locally: - Docker Compose starts a test database container - No retries (fail fast for debugging) - Can reuse an existing dev server See [`scripts/test-e2e.ts`](https://github.com/QuickDapp/QuickDapp/blob/v3.11.3/scripts/test-e2e.ts) for the test runner implementation and [`playwright.config.ts`](https://github.com/QuickDapp/QuickDapp/blob/v3.11.3/playwright.config.ts) for the full configuration. ## See Also For more information about E2E testing patterns and configuration, see the [E2E Testing](../testing/e2e) section. --- # Test The test command runs the integration test suite with database isolation and parallel execution. ## Running Tests ```shell bun run test # Run all tests bun run test --pattern auth # Run matching tests bun run test --watch # Watch mode bun run test --verbose # Debug logging bun run test --bail # Stop on first failure bun run test --timeout 60000 # Custom timeout (ms) bun run test -c 4 # Set concurrency (parallel workers) bun run test -f auth.test.ts # Run specific test file ``` ## How It Works Tests use PostgreSQL template databases for parallel execution. Before tests start, the runner pushes the schema to a template database. Each test file then gets its own clone of that template, providing complete isolation without the overhead of schema setup per file. Test files are ordered by duration (longest first) to optimize parallel execution. The `tests/test-run-order.json` file tracks this and is auto-updated after each run. ## Parallel Architecture Each test file receives: - A unique server port (base port + file index) - Its own database cloned from the template - An isolated `ServerApp` instance This allows multiple test files to run simultaneously without database conflicts. After completion, each cloned database is dropped. ## Test Structure ``` tests/ ├── helpers/ # Test utilities │ ├── server.ts # Server lifecycle │ ├── test-config.ts # Port and database assignment │ └── auth.ts # Auth helpers └── server/ # Integration tests ├── auth/ # Authentication tests └── graphql/ # GraphQL tests ``` ## Writing Tests ```typescript import "@tests/helpers/test-config" // Must be first import import { beforeAll, afterAll, test, expect } from 'bun:test' import { startTestServer } from '../helpers/server' import type { TestServer } from '../helpers/server' let testServer: TestServer beforeAll(async () => { testServer = await startTestServer() }) afterAll(async () => { await testServer.shutdown() }) test('example test', async () => { // Test implementation }) ``` The `@tests/helpers/test-config` import must come first—it sets environment variables (port, database URL) before `serverConfig` caches them at module load time. ## Debugging Enable verbose logging with a temporary config file: ```shell bun run test --verbose ``` This creates `.env.test.local` with `LOG_LEVEL=debug` and `WORKER_LOG_LEVEL=debug`, which is cleaned up after the run. For persistent debug config: ```shell echo "LOG_LEVEL=debug" > .env.test.local bun run test rm .env.test.local ``` ## Environment Tests use `.env.test` for configuration: ```bash NODE_ENV=test DATABASE_URL=postgresql://postgres:@localhost:5432/quickdapp_test ``` The test database should be separate from development to avoid data conflicts. ## See Also For detailed information about the test framework architecture, parallelization, and writing tests, see the [Testing](../testing) section. --- # Prod The production command runs the built application. It requires `bun run build` first. ## Running ```shell bun run prod # Run server and client preview bun run prod server # Server only (port 3000) bun run prod client # Client preview only (port 4173) ``` The server serves both the API and static files on port 3000. The client preview on port 4173 is for testing the frontend build independently. ## Environment Production mode uses `.env.production`: ```bash NODE_ENV=production DATABASE_URL=postgresql://user:password@prod-host:5432/quickdapp SESSION_ENCRYPTION_KEY=your_32_character_production_key ``` ## How it Works `bun run prod` runs `dist/server/binary.js`, which sets production environment defaults (via `EMBEDDED_ENV`) before loading the server. This ensures `NODE_ENV=production` is set before any configuration is evaluated. The binary entry point also extracts bundled static assets to a temp directory and serves them via the static file plugin. ## Binary vs bun run prod **`bun run prod`** runs `binary.js` with Bun. Use this for local testing or when Bun is installed on the server. **Binary deployment** uses self-contained executables with no dependencies: ```shell ./dist/binaries/quickdapp-linux-x64 ``` For production servers, prefer the binary approach for simplicity and reliability. ## Testing Production ```shell bun run build NODE_ENV=production bun run prod # Verify curl http://localhost:3000/health ``` --- # Database Database commands manage schema changes through DrizzleORM. Use `push` for development and `migrate` for production. ## Commands ### push Applies schema changes directly to the database without migration files: ```shell bun run db push # Apply changes bun run db push --force # Force destructive changes ``` Use during development for rapid iteration. This command is destructive and should never be used in production. ### generate Creates migration files from schema changes: ```shell bun run db generate ``` Run this before deploying to production. Migration files are saved in [`src/server/db/migrations/`](https://github.com/QuickDapp/QuickDapp/blob/v3.11.3/src/server/db/migrations/). ### migrate Runs pending migrations: ```shell bun run db migrate ``` Use in production for safe, versioned schema updates. This only applies previously generated migration files. ## Workflow ### Development ```shell # 1. Edit schema # src/server/db/schema.ts # 2. Generate types and push changes bun run gen bun run db push ``` ### Production Deployment ```shell # 1. Generate migrations (in development) bun run db generate # 2. Commit migration files git add src/server/db/migrations/ git commit -m "Add new migration" # 3. Deploy and migrate (on production server) NODE_ENV=production bun run db migrate ``` ## Configuration Database commands read from environment-specific files: ```bash # .env.development DATABASE_URL=postgresql://postgres:@localhost:5432/quickdapp_dev # .env.test DATABASE_URL=postgresql://postgres:@localhost:5432/quickdapp_test # .env.production DATABASE_URL=postgresql://user:pass@prod-host:5432/quickdapp ``` ## Troubleshooting **Connection failed**: ```shell psql "$DATABASE_URL" -c "SELECT 1;" ``` **Reset development database**: ```shell bun run db push --force ``` **Check migration status**: ```shell psql "$DATABASE_URL" -c "SELECT * FROM __drizzle_migrations;" ``` --- # Build The build command creates production-ready artifacts including optimized bundles and self-contained binaries. ## Building ```shell bun run build ``` This creates: ``` dist/ ├── client/ # Optimized frontend bundle │ ├── index.html │ └── assets/ # CSS, JS, images ├── server/ # Compiled server │ ├── index.js │ ├── binary.js │ └── binary-assets.json └── binaries/ # Self-contained executables ├── quickdapp-linux-x64 ├── quickdapp-linux-arm64 ├── quickdapp-darwin-x64 ├── quickdapp-darwin-arm64 └── quickdapp-windows-x64.exe ``` ## Options ```shell bun run build --no-clean # Keep previous build artifacts bun run build --bundle # Embed client in server for single-file serving ``` ## Production Environment Create `.env.production` for production configuration: ```bash NODE_ENV=production LOG_LEVEL=info DATABASE_URL=postgresql://user:password@prod-host:5432/quickdapp SESSION_ENCRYPTION_KEY=your_32_character_production_key WEB3_SERVER_WALLET_PRIVATE_KEY=0xYourProductionWalletKey CHAIN=sepolia WEB3_FACTORY_CONTRACT_ADDRESS=0xYourContractAddress WORKER_COUNT=cpus ``` ## Running the Build After building, run with: ```shell # Using bun (requires Bun on server) bun run prod # Using binary (no dependencies) ./dist/binaries/quickdapp-linux-x64 ``` ## Verifying Test the production build locally: ```shell bun run build NODE_ENV=production bun run prod curl http://localhost:3000/health ``` --- # Lint & Format QuickDapp uses [Biome](https://biomejs.dev/) for linting and formatting, and TypeScript's compiler for type checking. ## Commands ```shell bun run lint # Run type checking (tsc) + Biome linting bun run lint:fix # Auto-fix lint issues bun run format # Format code with Biome bun run typecheck # TypeScript type checking only ``` ## What Each Command Does **`bun run lint`** runs two checks: 1. TypeScript compiler (`tsc --noEmit`) to catch type errors 2. Biome linter to enforce code style rules **`bun run lint:fix`** runs Biome with auto-fix enabled, resolving issues like import ordering, unused imports, and style violations automatically. **`bun run format`** runs Biome's formatter to standardize code formatting (indentation, line breaks, trailing commas, etc.). ## Biome Configuration The project uses a single `biome.json` in the root folder. Key settings include: - 2-space indentation - No semicolons - Organized imports - Linting rules for correctness and style ## Integration with Git QuickDapp uses [husky](https://typicode.github.io/husky/) for git hooks. The pre-commit hook runs `lint:fix` and `format` on staged files to ensure committed code meets quality standards. Commits follow the [conventional commits](https://www.conventionalcommits.org/) format, enforced by [commitlint](https://commitlint.js.org/). --- # Command Line QuickDapp provides CLI commands for development, building, testing, and database management. All commands run through `bun run`. ## Quick Reference | Command | Description | |---------|-------------| | `bun run dev` | Start development server with hot reload | | `bun run build` | Build for production | | `bun run prod` | Run production server | | `bun run test` | Run test suite | | `bun run test:e2e` | Run Playwright E2E tests | | `bun run gen` | Generate types and migrations | | `bun run lint` | Type check and lint | | `bun run lint:fix` | Auto-fix lint issues | | `bun run format` | Format code | | `bun run db push` | Push schema to database | | `bun run db migrate` | Run migrations | ## Development ```shell bun run dev # Start with hot reload bun run dev --verbose # Detailed startup logging ``` The development server runs ElysiaJS on port 3000 with Vite on port 5173 (proxied through 3000). Both frontend and backend support hot reloading. ## Building ```shell bun run build # Production build with binaries ``` Creates optimized bundles in `dist/client/`, server code in `dist/server/`, and cross-platform binaries in `dist/binaries/`. ## Testing ```shell bun run test # Run all tests bun run test --pattern auth # Run matching tests bun run test --watch # Watch mode bun run test --verbose # Debug logging ``` Tests run with isolated database state. The test database is reset before each run. ## Code Generation ```shell bun run gen # Generate all types ``` Generates GraphQL TypeScript types and DrizzleORM migration files. ## Code Quality ```shell bun run lint # Check types and lint bun run lint:fix # Auto-fix issues bun run format # Format with Biome ``` ## Database ```shell bun run db push # Push schema (development) bun run db push --force # Force push (destructive) bun run db generate # Generate migration files bun run db migrate # Apply migrations (production) ``` Use `push` during development for quick iteration. Use `migrate` in production for safe, versioned changes. ## Environment Commands detect environment automatically. Override with `NODE_ENV`: ```shell NODE_ENV=production bun run build NODE_ENV=test bun run db push ``` Environment files load in order: `.env` → `.env.{NODE_ENV}` → `.env.local`. --- # Gen The `gen` command runs code generation for GraphQL types and database migrations. ## Usage ```shell bun run gen # Generate all types and migrations bun run gen --verbose # Detailed output ``` ## What It Generates The command runs two steps: **Step 1: Type generation** — Generates TypeScript types from the GraphQL schema and (in the Web3 variant) ABI types from Solidity contracts. This ensures type safety between the schema definitions and the TypeScript code that uses them. **Step 2: Database migrations** — Runs DrizzleORM's migration generator to detect schema changes in `src/server/db/schema.ts` and create SQL migration files in `src/server/db/migrations/`. ## When to Run Run `bun run gen` after: - Changing the GraphQL schema in `src/shared/graphql/schema.ts` - Modifying the database schema in `src/server/db/schema.ts` - Adding or updating types, queries, or mutations ## Automatic Generation The dev server automatically watches for schema changes and regenerates types. You typically only need to run `gen` manually when you want to create migration files for production deployment. See [`scripts/gen.ts`](https://github.com/QuickDapp/QuickDapp/blob/v3.11.3/scripts/gen.ts) for the implementation. --- # Dev The development server provides hot reloading for both frontend and backend, with integrated worker processes and WebSocket support. ## Starting ```shell bun run dev # Standard startup bun run dev --verbose # Detailed logging ``` This starts: - ElysiaJS backend on port 3000 - Vite frontend on port 5173 (proxied through 3000) - Worker processes for background jobs - WebSocket server at `/ws` ## Hot Reload **Backend changes** that reload automatically: - GraphQL resolvers - Route handlers - Utility functions **Changes requiring restart**: - Database schema - Worker job definitions - Server bootstrap logic **Frontend changes** reload instantly via Vite HMR: - React components - CSS/Tailwind - TypeScript files ## Environment Development uses layered configuration: ```shell .env # Base configuration .env.development # Development overrides .env.local # Personal overrides (gitignored) ``` Example `.env.development`: ```bash NODE_ENV=development LOG_LEVEL=debug DATABASE_URL=postgresql://postgres:@localhost:5432/quickdapp_dev CHAIN=anvil WEB3_ANVIL_RPC=http://localhost:8545 WORKER_COUNT=1 ``` ## Debugging Enable debug logging: ```shell LOG_LEVEL=debug WORKER_LOG_LEVEL=debug bun run dev ``` Run with reduced workers for easier debugging: ```shell WORKER_COUNT=1 bun run dev ``` ## Database Workflow After modifying [`src/server/db/schema.ts`](https://github.com/QuickDapp/QuickDapp/blob/v3.11.3/src/server/db/schema.ts): ```shell bun run gen # Generate types and migrations bun run db push # Push changes to development database ``` ## Troubleshooting **Port in use**: ```shell lsof -ti:3000 | xargs kill -9 ``` **Database connection failed**: ```shell brew services list | grep postgresql ``` **Hot reload not working**: ```shell rm -rf node_modules/.vite bun run dev ``` --- # Introduction **QuickDapp** is a "boilerplate" that serves as the foundation for any modern web app you want to build. Whether you're vibe coding or coding by hand, QuickDapp helps you quickly build _and_ deploy web applications - _"batteries included"_. It is designed to save you a massive amount of time and effort, freeing you up to focus on the parts of your web app that actually matter. :::note A _boilerplate_ is a standardized, reusable piece code that can be used to build multiple projects. Think of it as a design template or blueprint from which you build the actual specific web app you're interested in making. ::: ## Why do I need this? Can't I just vibe code? Vibe-coding is great. It saves time and gets the job done. A lot of vibe coding when into QuickDapp, trust us ;) But when you're vibe coding more than once you'll find that the AI is recreating the same stuff over and over again - like user account management, theming, mobile-friendly UI components, etc. It would get the job done even quicker if it didn't have to repeat previous work. This is exactly where QuickDapp comes in. QuickDapp is a working web application out-of-the-box with user authentication, testing, database connectivity, email sending, real-time notifications, basically a whole bunch of stuff that the AI doesn't need to figure out. It's all there so you can straight away just build the stuff that makes your app unique. _"Don't repeat yourself" (DRY)_ is a principle of software development which dictates that stuff that doesn't need to change should be re-used. This is why, even when vibe coding, an AI will use common frameworks like React.js to build your app. But that's not all. Vibe coding often doesn't (yet) have a neat solution for deployment. It's one thing to build your app. It's a whole other challenge to be able to deploy it to the cloud in a way that makes scaling easy. QuickDapp has got your back on this. Your entire web app gets built into a single executable binary (yes, you read that right) for multiple platforms, making it very easy to run your app. Additionally, QuickDapp can create a [Docker container](https://docker.com/), allowing for deployment to _any_ cloud provider. ## Ok, what do I get with QuickDapp? QuickDapp uses all of the following: * [TypeScript](https://www.typescriptlang.org/) as the programming language. * [Bun](https://bun.sh/) as the package manager for installing third-party dependencies, running the web app and building the production output. * Backend: * [ElysiaJS](https://elysiajs.com/) as the backend framework. * [PostgreSQL](https://www.postgresql.org/) as the database. * [DrizzleORM](https://orm.drizzle.team/) for structured database querying. * [GraphQL Yoga](https://the-guild.dev/graphql/yoga-server) for exposing a GraphQL API. * Frontend: * [React](https://react.dev/) as the frontend framework. * [TailwindCSS](https://tailwindcss.com/) for styling. * [Radix UI](https://www.radix-ui.com/) for UI components * [React Query](https://tanstack.com/query/latest) for calls to the GraphQL backend API. Additionally, QuickDapp provides the following features: * Authenticate users by email or OAuth (Google, Facebook, etc) without using cookies. * Background workers so that you can asynchronously schedule long-running tasks. * Websockets integration for real-time chat and notifications. * Parallelized testing framework for fast automated integration tests. * Single-executable binary builds of your entire app for easy distribution. * Dark and light theme support with system preference detection. QuickDapp also supports [variants](./variants) — specialized derivations that add domain-specific features. For example, the [Web3 variant](./variants/web3) adds blockchain wallet authentication, smart contract interactions, and on-chain event monitoring. As you can see it is a very opinionated design, meaning that we think all of the above together form a decent foundational layer for any kind of web app you want to build. Having said that, you can change _anything_ you don't like in QuickDapp to suit your own needs. ## What if I don't like something? As a boilerplate QuickDapp is just a starting point. It comes with the entire source code. You can choose to stick to the structure and design decisions QuickDapp comes with or you can rewrite it any way you want (and then you have your own custom boilerplate based on QuickDapp!). On the whole though, we don't think you will need to rewrite QuickDapp that much since we've used it build multiple apps ourselves. We've incorporated the lessons learnt from building those apps into QuickDapp's design choices. Moreover, QuickDapp has support for "variants". You can find out more about those in the [Variants documentation](./variants). ## Ok, how do I get started? The next section - [Getting Started](./getting-started) - will get you up and running quickly. The remainder of this documentation gives you a thorough understanding of all the different parts of QuickDapp and how to get the most out of it. --- # Architecture Layout QuickDapp follows a modern, clean architecture built around the **ServerApp dependency injection pattern**. This document provides an overview of how all the pieces fit together. ## High-level Architecture ``` ┌─────────────────────────────────────────────────────────────┐ │ QuickDapp │ ├─────────────────────────────────────────────────────────────┤ │ Frontend (React + Vite) │ │ ├── React 19 + TypeScript │ │ ├── GraphQL Client (React Query) │ │ └── WebSocket Client │ ├─────────────────────────────────────────────────────────────┤ │ Backend (Bun + ElysiaJS) │ │ ├── ElysiaJS Server │ │ ├── GraphQL Yoga API │ │ ├── JWT Authentication │ │ ├── WebSocket Server │ │ └── Static Asset Serving │ ├─────────────────────────────────────────────────────────────┤ │ Worker System (Child Processes) │ │ ├── Background Job Processing │ │ ├── Cron Job Scheduling │ │ └── IPC Communication │ ├─────────────────────────────────────────────────────────────┤ │ Database Layer (DrizzleORM + PostgreSQL) │ │ ├── Type-safe SQL Queries │ │ ├── Schema Migrations │ │ └── Connection Pooling │ ├─────────────────────────────────────────────────────────────┤ │ Variant-specific Layers (Optional) │ │ └── e.g. Web3: Blockchain Clients, Contract Interactions │ └─────────────────────────────────────────────────────────────┘ ``` ## The ServerApp Pattern The core architectural innovation in QuickDapp is the **ServerApp pattern** - a dependency injection system that provides clean access to shared resources across all components. ```typescript export type ServerApp = { app: Elysia // ElysiaJS server instance db: Database // DrizzleORM database connection rootLogger: Logger // Root logger instance createLogger: (category: string) => Logger // Logger factory startSpan: typeof startSpan // Sentry performance monitoring workerManager: WorkerManager // Background job processing socketManager: ISocketManager // WebSocket manager createNotification: (userId: number, data: NotificationData) => Promise } ``` :::note Variants extend `ServerApp` with additional fields. For example, the Web3 variant adds `publicClient` and `walletClient` for blockchain access. ::: **Benefits of this pattern:** * **Clean dependencies** — No global state or singletons * **Easy testing** — Mock individual services for unit tests * **Type safety** — Full TypeScript support across all layers * **Consistent access** — Same interface for all components ## Directory Structure ``` src/ ├── server/ # Backend server code │ ├── db/ # Database schema, migrations, and queries │ ├── graphql/ # GraphQL resolvers and schema │ ├── auth/ # Authentication (email, OAuth) │ ├── workers/ # Background job system │ ├── lib/ # Server utilities (logging, errors, etc.) │ └── ws/ # WebSocket implementation ├── client/ # Frontend React application │ ├── components/ # React components │ ├── contexts/ # Theme, Auth, Socket contexts │ ├── hooks/ # Custom React hooks │ ├── lib/ # Client-side utilities │ └── pages/ # Application pages ├── shared/ # Code shared between client/server │ ├── config/ # Environment configuration │ └── graphql/ # GraphQL schema definitions scripts/ # Build and development scripts tests/ # Integration test suite ``` ## Technology Stack ### Runtime & Server * **Bun** — Primary runtime and package manager * **ElysiaJS** — High-performance web framework with native WebSocket support * **GraphQL Yoga** — GraphQL server (no GraphQL subscriptions; use WebSockets for realtime) ### Frontend * **React 19** — Latest React with concurrent features * **Vite** — Lightning-fast build tool and dev server * **TypeScript** — Full type safety across the stack ### Database & ORM * **PostgreSQL** — Robust relational database * **DrizzleORM** — Type-safe, performant SQL toolkit * **postgres** — High-performance PostgreSQL client ### Authentication & Security * **JWT (Jose)** — Stateless token authentication * **Multiple auth providers** — Email and OAuth (Google, Facebook, GitHub, X, TikTok, LinkedIn) * **Custom auth directive** — GraphQL operation-level security ### Background Processing * **Child process architecture** — Isolated worker processes * **IPC communication** — Inter-process messaging * **Database job queue** — Persistent job storage with retry logic ## Request Flow ### GraphQL API Request ``` 1. Frontend sends GraphQL query 2. ElysiaJS receives request 3. GraphQL Yoga parses and validates 4. Auth directive checks JWT token 5. Resolver function executes 6. Database query via DrizzleORM 7. Response returned to client ``` ### WebSocket Connection ``` 1. Client establishes WebSocket connection 2. SocketManager handles connection 3. User authentication via JWT 4. Real-time message routing 5. Notifications sent to specific users ``` ### Background Job Processing ``` 1. Job submitted to database queue 2. Worker process picks up job 3. Job handler executes business logic 4. Results stored and notifications sent 5. Job marked complete or retry scheduled ``` ## Configuration Management QuickDapp uses a layered configuration system: ``` .env # Base configuration (committed) .env.{NODE_ENV} # Environment-specific overrides .env.{NODE_ENV}.local # Environment-specific local overrides .env.local # Local developer overrides (gitignored) ``` Configuration is loaded by the shared bootstrap pattern and provides type-safe access via: * `serverConfig` — Server-side configuration * `clientConfig` — Client-side configuration (subset of server config) ## Development Workflow ### Development Mode * **Hot reload** — Both frontend and backend update automatically * **Database migrations** — Schema changes applied instantly * **Worker monitoring** — Background jobs visible in logs * **GraphQL playground** — Interactive API exploration ### Testing * **Integration tests** — Full end-to-end testing * **Database isolation** — Each test gets a clean database clone * **Parallel execution** — Template database pattern for fast test runs * **Mock services** — Authentication and external services mocked ### Production Build * **Binary compilation** — Self-contained executables * **Asset bundling** — Frontend assets embedded in server * **Database migrations** — Production-safe schema updates * **Docker support** — Container-based deployment ## Data Flow ### Authentication Flow ``` 1. User initiates login (email or OAuth) 2. Auth provider validates credentials 3. Backend verifies identity 4. JWT token issued and stored 5. Token used for subsequent requests ``` ### Database Operations ``` 1. TypeScript schema defines structure 2. DrizzleORM generates migrations 3. Migrations applied to database 4. Type-safe queries in application code 5. Connection pooling handles concurrency ``` ### Real-time Updates ``` 1. Server event occurs (job completion, etc.) 2. Notification created via ServerApp 3. SocketManager routes to connected users 4. Frontend receives WebSocket message 5. UI updates reactively ``` This architecture provides a solid foundation for building scalable, maintainable web applications with modern development practices and excellent developer experience. [Variants](./variants) can extend this foundation with domain-specific capabilities. --- # Environment Variables QuickDapp uses environment variables for configuration, loaded from `.env` files based on `NODE_ENV`. The base `.env` file is loaded first, then environment-specific overrides (`.env.development`, `.env.test`, `.env.production`), and finally `.env.local` for developer overrides. ## Required Variables Every QuickDapp deployment needs these core variables: | Variable | Description | |----------|-------------| | `DATABASE_URL` | PostgreSQL connection string. See [Database](./backend/database) for schema and query patterns. | | `SESSION_ENCRYPTION_KEY` | Secret key for JWT signing and OAuth state encryption. Must be at least 32 characters. | | `API_URL` | Base URL for the API (e.g., `http://localhost:3000` or `https://api.yourdomain.com`). | ```bash DATABASE_URL=postgresql://user:password@host:5432/database SESSION_ENCRYPTION_KEY=your_min_32_characters_key API_URL=http://localhost:3000 ``` ## Server Configuration | Variable | Default | Description | |----------|---------|-------------| | `WEB_ENABLED` | `true` | Enable the web server. Set to `false` for worker-only processes. | | `HOST` | `localhost` | Server bind address. Use `0.0.0.0` in containers. | | `PORT` | `3000` | Server port number. | | `WORKER_COUNT` | `1` | Number of worker processes. Set to `"cpus"` to match CPU cores. | | `STATIC_ASSETS_FOLDER` | — | Custom path for static assets (overrides default). | | `LOG_LEVEL` | `info` | Logging verbosity: `trace`, `debug`, `info`, `warn`, `error`. | | `WORKER_LOG_LEVEL` | `info` | Log level for worker processes. | ```bash WEB_ENABLED=true HOST=localhost PORT=3000 WORKER_COUNT=1 LOG_LEVEL=info WORKER_LOG_LEVEL=info ``` ## OAuth Providers Configure OAuth authentication by setting client credentials for each provider. Leave empty to disable a provider. See [Authentication](./backend/authentication) for implementation details. | Variable | Description | |----------|-------------| | `OAUTH_CALLBACK_BASE_URL` | Base URL for OAuth callbacks (e.g., `http://localhost:3000`). | | `OAUTH_GOOGLE_CLIENT_ID` | Google OAuth client ID. | | `OAUTH_GOOGLE_CLIENT_SECRET` | Google OAuth client secret. | | `OAUTH_FACEBOOK_CLIENT_ID` | Facebook OAuth app ID. | | `OAUTH_FACEBOOK_CLIENT_SECRET` | Facebook OAuth app secret. | | `OAUTH_GITHUB_CLIENT_ID` | GitHub OAuth app client ID. | | `OAUTH_GITHUB_CLIENT_SECRET` | GitHub OAuth app client secret. | | `OAUTH_X_CLIENT_ID` | X (Twitter) OAuth client ID. | | `OAUTH_X_CLIENT_SECRET` | X (Twitter) OAuth client secret. | | `OAUTH_TIKTOK_CLIENT_KEY` | TikTok OAuth client key. | | `OAUTH_TIKTOK_CLIENT_SECRET` | TikTok OAuth client secret. | | `OAUTH_LINKEDIN_CLIENT_ID` | LinkedIn OAuth client ID. | | `OAUTH_LINKEDIN_CLIENT_SECRET` | LinkedIn OAuth client secret. | ## Email (Mailgun) Configure transactional email delivery via Mailgun. When not configured, emails are logged to the console instead of sent. See [Mailgun](./backend/mailgun) for usage. | Variable | Description | |----------|-------------| | `MAILGUN_API_KEY` | Mailgun API key for sending emails. | | `MAILGUN_API_ENDPOINT` | Mailgun API endpoint. Set for EU region: `https://api.eu.mailgun.net`. | | `MAILGUN_FROM_ADDRESS` | Sender email address (e.g., `noreply@yourdomain.com`). Domain is extracted automatically. | ## Error Tracking (Sentry) Configure Sentry for error tracking and performance monitoring. See [Sentry](./backend/sentry) for integration details. | Variable | Default | Description | |----------|---------|-------------| | `SENTRY_DSN` | — | Sentry DSN for the main server process. | | `SENTRY_WORKER_DSN` | — | Sentry DSN for worker processes (can be same or different project). | | `SENTRY_TRACES_SAMPLE_RATE` | `1.0` | Fraction of requests to trace (0.0 to 1.0). | | `SENTRY_PROFILE_SESSION_SAMPLE_RATE` | `1.0` | Fraction of sessions to profile (0.0 to 1.0). | ## WebSocket Configuration Control real-time connection limits. See [WebSockets](./backend/websockets) for the connection lifecycle and message types. | Variable | Default | Description | |----------|---------|-------------| | `SOCKET_MAX_CONNECTIONS_PER_USER` | `5` | Maximum concurrent WebSocket connections per user. | | `SOCKET_MAX_TOTAL_CONNECTIONS` | `10000` | Global limit for all WebSocket connections. | ## Client-Visible Variables These variables are safe to expose in the frontend bundle: - `APP_NAME`, `APP_VERSION`, `NODE_ENV` - `API_URL` - `SENTRY_DSN` Server-side secrets like database credentials, private keys, and OAuth secrets remain server-only. See [`src/shared/config/client.ts`](https://github.com/QuickDapp/QuickDapp/blob/v3.11.3/src/shared/config/client.ts) for the client configuration and [`src/shared/config/server.ts`](https://github.com/QuickDapp/QuickDapp/blob/v3.11.3/src/shared/config/server.ts) for the full server configuration. --- ## Web3 Variant Variables :::note The following variables are only relevant when using the [Web3 variant](./variants/web3). They are not present in the base package. ::: | Variable | Description | |----------|-------------| | `WEB3_ENABLED` | Enable Web3 features (always `true` in the Web3 variant). | | `WEB3_SERVER_WALLET_PRIVATE_KEY` | Private key for server-side blockchain transactions. Use a dedicated wallet. | | `WEB3_SUPPORTED_CHAINS` | Comma-separated chain names (e.g., `anvil`, `mainnet`, `sepolia`, `base`). | | `WEB3_WALLETCONNECT_PROJECT_ID` | WalletConnect Cloud project ID for wallet connections. | | `WEB3_ALLOWED_SIWE_ORIGINS` | Comma-separated origins allowed for SIWE authentication. | | `WEB3_FACTORY_CONTRACT_ADDRESS` | Deployed factory contract address. | ```bash WEB3_ENABLED=true WEB3_SERVER_WALLET_PRIVATE_KEY=0xYourWalletPrivateKey WEB3_SUPPORTED_CHAINS=anvil WEB3_WALLETCONNECT_PROJECT_ID=your_walletconnect_project_id WEB3_ALLOWED_SIWE_ORIGINS=http://localhost:3000 WEB3_FACTORY_CONTRACT_ADDRESS=0xYourContractAddress ``` ### Per-Chain RPC Endpoints Server-side blockchain operations use chain-specific RPC variables: | Variable | Description | |----------|-------------| | `WEB3_ANVIL_RPC` | RPC URL for local Anvil/Foundry development network. | | `WEB3_MAINNET_RPC` | RPC URL for Ethereum mainnet. | | `WEB3_SEPOLIA_RPC` | RPC URL for Sepolia testnet. | | `WEB3_BASE_RPC` | RPC URL for Base network. | ```bash WEB3_ANVIL_RPC=http://localhost:8545 WEB3_MAINNET_RPC=https://eth.llamarpc.com WEB3_SEPOLIA_RPC=https://rpc.sepolia.org WEB3_BASE_RPC=https://mainnet.base.org ``` Client-side uses viem's built-in public RPCs for each chain. ### Web3 Client-Visible Variables These Web3 variables are also exposed to the frontend bundle: - `WEB3_ENABLED`, `WEB3_SUPPORTED_CHAINS`, `WEB3_WALLETCONNECT_PROJECT_ID`, `WEB3_FACTORY_CONTRACT_ADDRESS` ---