Skip to main content

Quick Start

Prerequisites

  • A Civic Auth Client ID (get it from auth.civic.com)
  • Configure your redirect URL in the Civic Auth dashboard (typically http://localhost:3000 for development)
If you plan to use Web3 features, select “Auth + Web3” from the tabs below.

Installation

npm install @civic/auth
We highly recommend using Vite for the best development experience with modern JavaScript features, fast hot reloading, and seamless ES module support.
npm create vite@latest my-civic-app -- --template vanilla
cd my-civic-app
npm install
npm install @civic/auth

Simple Setup

  1. HTML:
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>My App with Civic Auth</title>
  </head>
  <body>
    <h1>My App</h1>

    <button id="loginButton">Sign In</button>
    <button id="logoutButton">Sign Out</button>

    <!-- Only needed for embedded display mode -->
    <div id="authContainer"></div>

    <script type="module" src="main.js"></script>
  </body>
</html>
  1. JavaScript (main.js):
import { CivicAuth } from "@civic/auth/vanillajs";

// Initialize auth directly with top-level await
const authClient = await CivicAuth.create({
  clientId: "YOUR_CLIENT_ID",
});

// Sign in
document.getElementById("loginButton").addEventListener("click", async () => {
  try {
    const { user } = await authClient.startAuthentication();
  } catch (error) {
    console.error("Authentication failed:", error);
  }
});

// Sign out
document.getElementById("logoutButton").addEventListener("click", async () => {
  await authClient?.logout();
});
That’s it! Replace YOUR_CLIENT_ID with your actual client ID and you’re done.

Alternative: Backend Integration

If you prefer backend session management, you can configure the client to get login URLs from your Express backend. The magic is the loginUrl option:
import { CivicAuth } from "@civic/auth/vanillajs";

// Configure client to use your backend for login URLs
const authClient = await CivicAuth.create({
  loginUrl: "https://your-backend.com/auth/login-url", // The magic!
});

// Now authentication works through your backend
const { user } = await authClient.startAuthentication();

Custom Backend Endpoints

When using backend integration, you can customize the API endpoints that the client calls on your backend:
import { CivicAuth } from "@civic/auth/vanillajs";

const authClient = await CivicAuth.create({
  loginUrl: "https://your-backend.com/auth/login-url",
  backendEndpoints: {
    refresh: "/api/v1/auth/refresh", // default: "/auth/refresh"
    logout: "/api/v1/auth/logout", // default: "/auth/logout"
    user: "/api/v1/auth/user", // default: "/auth/user"
  },
});
The backendEndpoints configuration is only used when loginUrl is provided. Each endpoint is optional - if not specified, the default will be used.

Configuration Options

FieldRequiredDefaultDescription
clientIdYes-Your Civic Auth client ID from auth.civic.com
targetContainerElementNo-DOM element where embedded iframe will be rendered
redirectUrlNoCurrent URLOAuth redirect URL after authentication
displayModeNomodalHow the auth UI is displayed: embedded, modal, redirect, or new_tab
scopesNo['openid', 'profile', 'email']OAuth scopes to request
loginUrlNo-Backend URL for login redirect (enables backend integration)
backendEndpointsNoSee belowCustom backend API endpoints (only used with loginUrl)

Backend Endpoints Default Values

When using loginUrl for backend integration, the following default endpoints are used:
backendEndpoints: {
  refresh: "/auth/refresh",  // Token refresh endpoint
  logout: "/auth/logout",    // Logout endpoint
  user: "/auth/user"         // User info endpoint
}

Display Modes

The displayMode option controls how the authentication UI is presented:
  • embedded (default): The auth UI loads in an iframe within your specified container element
  • modal: The auth UI opens in a modal overlay on top of your current page
  • redirect: Full page navigation to the Civic auth server and back to your site
  • new_tab: Opens auth flow in a new browser tab/popup window

Logout

Logging out is very simple.
const logout = async () => {
  await authClient?.logout();
  // ...
};
User object access:
  • Use authClient.getCurrentUser() to retrieve current user information before logout
  • Use authClient.isAuthenticated() to check if user is currently logged in

Web3 Wallets

Web3 wallet functionality is available when using the @civic/auth-web3 package. This enables embedded Ethereum and Solana wallets for your users.

Creating an Embedded Wallet

When a user logs in, they don’t have a Web3 wallet by default. You can create an embedded wallet for them:
import { CivicAuth, userHasWallet } from "@civic/auth-web3/vanillajs";

const authClient = await CivicAuth.create({
  clientId: "YOUR_CLIENT_ID",
});

// After authentication
const user = await authClient.getCurrentUser();

if (user && !userHasWallet(user)) {
  await user.createWallet();
  console.log("Wallet created!");
}

Accessing the Wallet

Once a wallet is created, you can access it directly from the user object:
const user = await authClient.getCurrentUser();

if (userHasWallet(user)) {
  const { address, wallet } = user.ethereum;
  
  console.log("Wallet address:", address);
  
  // Send a transaction
  const hash = await wallet.sendTransaction({
    to: "0x742d35Cc6635C0532925a3b8D1b5d2e36c6e7C1c",
    value: "1000000000000000000" // 1 ETH in wei
  });
  
  console.log("Transaction hash:", hash);
}

Checking Wallet Balance

// Using the built-in balance method
const user = await authClient.getCurrentUser();

if (userHasWallet(user)) {
  const balance = await user.ethereum.wallet.getBalance();
  console.log("Balance:", balance, "ETH");
}

Signing Messages

const user = await authClient.getCurrentUser();

if (userHasWallet(user)) {
  const message = "Hello, Web3!";
  const signature = await user.ethereum.wallet.signMessage(message);
  console.log("Message signature:", signature);
}

Complete Example

Here’s a complete example showing authentication and wallet creation:
import { CivicAuth, userHasWallet } from "@civic/auth-web3/vanillajs";

// Initialize with Ethereum support
const authClient = await CivicAuth.create({
  clientId: "YOUR_CLIENT_ID",
});

// Auth flow
document.getElementById("loginButton").addEventListener("click", async () => {
  try {
    const { user } = await authClient.startAuthentication();
    
    // Create wallet if user doesn't have one
    if (user && !userHasWallet(user)) {
      console.log("Creating wallet...");
      await user.createWallet();
    }
    
    // Display wallet info
    if (userHasWallet(user)) {
      document.getElementById("walletAddress").textContent = user.ethereum.address;
      
      const balance = await user.ethereum.wallet.getBalance();
      document.getElementById("walletBalance").textContent = `${balance} ETH`;
    }
    
  } catch (error) {
    console.error("Authentication failed:", error);
  }
});

// Send transaction
document.getElementById("sendButton").addEventListener("click", async () => {
  const user = await authClient.getCurrentUser();
  
  if (userHasWallet(user)) {
    const recipient = document.getElementById("recipient").value;
    const amount = document.getElementById("amount").value;
    
    try {
      const hash = await user.ethereum.wallet.sendTransaction({
        to: recipient,
        value: (parseFloat(amount) * 1e18).toString() // Convert ETH to wei
      });
      
      console.log("Transaction sent:", hash);
    } catch (error) {
      console.error("Transaction failed:", error);
    }
  }
});

Multi-Chain Configuration

Unlike React apps that use Wagmi for chain management, vanilla JavaScript handles chain configuration directly. You can configure multiple chains and switch between them:
import { CivicAuth } from "@civic/auth-web3/vanillajs";
import { mainnet, sepolia, polygon } from "@civic/auth-web3/chains";

const authClient = await CivicAuth.create({
  clientId: "YOUR_CLIENT_ID",
  chains: [mainnet, sepolia, polygon],
  defaultChain: mainnet,
  transports: {
    [mainnet.id]: {
      http: ["https://eth-mainnet.g.alchemy.com/v2/YOUR-API-KEY"],
    },
    [sepolia.id]: {
      http: ["https://eth-sepolia.g.alchemy.com/v2/YOUR-API-KEY"],
    },
    [polygon.id]: {
      http: ["https://polygon-mainnet.g.alchemy.com/v2/YOUR-API-KEY"],
      webSocket: ["wss://polygon-mainnet.g.alchemy.com/v2/YOUR-API-KEY"],
    },
  },
});

// Switch chains programmatically
await authClient.switchChain(sepolia.id);

// Get current chain
const currentChain = await authClient.getCurrentChain();
console.log("Current chain:", currentChain.name);

Advanced Chain Configuration

For more control over chain behavior, you can configure additional options:
const authClient = await CivicAuth.create({
  clientId: "YOUR_CLIENT_ID",
  chains: [
    {
      id: 1,
      name: "Ethereum",
      nativeCurrency: { name: "Ether", symbol: "ETH", decimals: 18 },
      rpcUrls: {
        default: { http: ["https://your-custom-rpc.com"] },
        public: { http: ["https://ethereum.publicnode.com"] },
      },
      blockExplorers: {
        default: { name: "Etherscan", url: "https://etherscan.io" },
      },
    },
    // Custom L2 or testnet
    {
      id: 31337,
      name: "Local Chain",
      nativeCurrency: { name: "Ether", symbol: "ETH", decimals: 18 },
      rpcUrls: {
        default: { http: ["http://127.0.0.1:8545"] },
      },
    },
  ],
});

Event Handling

Since vanilla JS doesn’t have reactive hooks like React, comprehensive event handling is crucial for building responsive UIs:

Authentication Events

// User authentication state changes
authClient.on("user", (user) => {
  if (user) {
    console.log("User logged in:", user.name);
    document.getElementById("userInfo").style.display = "block";
    document.getElementById("loginButton").style.display = "none";
  } else {
    console.log("User logged out");
    document.getElementById("userInfo").style.display = "none";
    document.getElementById("loginButton").style.display = "block";
  }
});

// Authentication completed successfully
authClient.on("authenticated", (user) => {
  console.log("Authentication successful");
  // Auto-create wallet for new users
  if (!userHasWallet(user)) {
    user.createWallet();
  }
});

// Authentication failed
authClient.on("authError", (error) => {
  console.error("Authentication failed:", error);
  document.getElementById("errorMessage").textContent = error.message;
});

// User logged out
authClient.on("unauthenticated", () => {
  console.log("User signed out");
  clearWalletUI();
});

Wallet Events

// Wallet created for user
authClient.on("walletCreated", (walletInfo) => {
  console.log("Wallet created:", walletInfo.address);
  document.getElementById("walletAddress").textContent = walletInfo.address;
  updateBalance(); // Refresh balance display
});

// Wallet connected/ready to use
authClient.on("walletConnected", (wallet) => {
  console.log("Wallet connected:", wallet.address);
  document.getElementById("walletStatus").textContent = "Connected";
  document.getElementById("sendButton").disabled = false;
});

// Wallet disconnected
authClient.on("walletDisconnected", () => {
  console.log("Wallet disconnected");
  document.getElementById("walletStatus").textContent = "Disconnected";
  document.getElementById("sendButton").disabled = true;
});

// Wallet operation failed
authClient.on("walletError", (error) => {
  console.error("Wallet error:", error);
  document.getElementById("walletError").textContent = error.message;
});

Chain & Network Events

// Chain changed (Ethereum)
authClient.on("chainChanged", (chainId) => {
  console.log("Chain changed to:", chainId);
  updateUIForChain(chainId);
  updateBalance(); // Balance might be different on new chain
});

// Network changed (Solana)
authClient.on("networkChanged", (network) => {
  console.log("Network changed to:", network);
  updateUIForNetwork(network);
  updateBalance();
});

// Account changed (user switched wallets)
authClient.on("accountsChanged", (accounts) => {
  if (accounts.length === 0) {
    console.log("No accounts available");
  } else {
    console.log("Active account:", accounts[0]);
    document.getElementById("walletAddress").textContent = accounts[0];
    updateBalance();
  }
});

Transaction Events

// Transaction started
authClient.on("transactionStarted", (txInfo) => {
  console.log("Transaction started:", txInfo.hash);
  document.getElementById("txStatus").textContent = "Sending...";
  document.getElementById("txHash").textContent = txInfo.hash;
});

// Transaction confirmed
authClient.on("transactionConfirmed", (txInfo) => {
  console.log("Transaction confirmed:", txInfo.hash);
  document.getElementById("txStatus").textContent = "Confirmed";
  updateBalance(); // Refresh balance after transaction
});

// Transaction failed
authClient.on("transactionFailed", (error) => {
  console.error("Transaction failed:", error);
  document.getElementById("txStatus").textContent = "Failed: " + error.message;
});

// Transaction pending (waiting for confirmation)
authClient.on("transactionPending", (txInfo) => {
  console.log("Transaction pending:", txInfo.hash);
  document.getElementById("txStatus").textContent = "Pending confirmation...";
});

Balance Events

// Balance changed (useful for real-time updates)
authClient.on("balanceChanged", (balanceInfo) => {
  console.log("Balance updated:", balanceInfo);
  const { balance, symbol, address } = balanceInfo;
  document.getElementById("balance").textContent = `${balance} ${symbol}`;
});

Connection Events

// Initial connection established
authClient.on("connect", () => {
  console.log("Connected to Civic Auth");
  document.getElementById("connectionStatus").textContent = "Connected";
});

// Connection lost
authClient.on("disconnect", () => {
  console.log("Disconnected from Civic Auth");
  document.getElementById("connectionStatus").textContent = "Disconnected";
});

// Reconnected after connection loss
authClient.on("reconnect", () => {
  console.log("Reconnected to Civic Auth");
  document.getElementById("connectionStatus").textContent = "Reconnected";
  // Refresh user state
  authClient.getCurrentUser().then(updateUserUI);
});

Complete Event-Driven Example

// Set up comprehensive event handling
function setupEventListeners() {
  // Authentication flow
  authClient.on("authenticated", async (user) => {
    updateUserUI(user);
    if (!userHasWallet(user)) {
      await user.createWallet();
    }
  });

  authClient.on("walletCreated", (walletInfo) => {
    updateWalletUI(walletInfo);
    updateBalance();
  });

  // Real-time updates
  authClient.on("balanceChanged", updateBalanceUI);
  authClient.on("chainChanged", handleChainChange);
  authClient.on("transactionConfirmed", () => {
    showNotification("Transaction confirmed!");
    updateBalance();
  });

  // Error handling
  authClient.on("authError", handleAuthError);
  authClient.on("walletError", handleWalletError);
  authClient.on("transactionFailed", handleTransactionError);
}

// Helper functions
function updateUserUI(user) {
  document.getElementById("userName").textContent = user.name;
  document.getElementById("userEmail").textContent = user.email;
}

function updateWalletUI(walletInfo) {
  document.getElementById("walletAddress").textContent = walletInfo.address;
  document.getElementById("walletSection").style.display = "block";
}

function updateBalanceUI(balanceInfo) {
  document.getElementById("balance").textContent = 
    `${balanceInfo.balance} ${balanceInfo.symbol}`;
}

function handleChainChange(chainId) {
  const chainName = getChainName(chainId);
  document.getElementById("currentChain").textContent = chainName;
  updateBalance(); // Balance might be different on new chain
}

// Initialize events
setupEventListeners();

Configuration Options Reference

FieldTypeDefaultDescription
blockchainstringethereumChoose "ethereum" or "solana"
chainsChain[][mainnet]Array of supported chains (Ethereum only)
defaultChainChainFirst chainDefault chain to use
transportsobjectPublic RPCsRPC configurations per chain
networksobjectMainnetNetwork configurations (Solana only)
autoConnectbooleantrueAutomatically connect wallet after authentication

Simple Configuration (Single Chain)

For simple single-chain apps, you can use the minimal configuration:
const authClient = await CivicAuth.create({
  clientId: "YOUR_CLIENT_ID",
  // Uses Ethereum mainnet by default
});
Security Note: Embedded wallets are non-custodial. Neither Civic nor your application ever has access to the wallet’s private keys. The wallet is generated and managed securely by our infrastructure partners.

Troubleshooting

Module Resolution Error

If you encounter an error like Failed to resolve module specifier "@civic/auth/vanillajs", this is typically caused by a corrupted module cache or installation issue. Solution:
# Clear node_modules and package-lock.json, then reinstall
rm -rf node_modules package-lock.json
npm install
For Vite users:
# Also clear Vite's cache
rm -rf node_modules package-lock.json .vite
npm install
This issue can occur when switching between different versions of the @civic/auth package or when the package installation is interrupted.

Common Issues

  • CORS errors: Ensure your redirect URL in the Civic Auth dashboard exactly matches your development server URL
  • Authentication not starting: Verify your client ID is correct and your redirect URL is properly configured
  • Container element not found: Make sure the target container element exists in the DOM before initializing Civic Auth

API Reference

CivicAuth Class

startAuthentication()

Initiates the authentication process. Returns: Promise that resolves when authentication completes or rejects on error

getCurrentUser()

Retrieves the current authenticated user’s information. Returns: Promise that resolves to a user object or null if not authenticated

isAuthenticated()

Checks if a user is currently authenticated. Returns: Promise that resolves to a boolean indicating authentication status

logout()

Logs out the user. Returns a boolean or throws an error if unsuccessful