Quick Start
/
5-Minute Integration

5-Minute Integration

Minimal end-to-end passwordless login

What you'll build

  • A browser login flow driven by Hawcx steps
  • A backend exchange endpoint using the Hawcx backend SDK
  • An authenticated session in your app

Flow overview

Loading diagram...

Prerequisites

From your Hawcx Dashboard, grab:

  • Config ID — your client identifier

You’ll set up a small backend exchange endpoint in Backend — Exchange Code.


Step 1: Install

Frontend SDK

Choose React or Vanilla JS:

npm install @hawcx/react
npm install @hawcx/core

Backend SDK

Choose your backend language:

npm install @hawcx/oauth-client
pip install hawcx-oauth-client

Step 2: Configure Environment

Set these before writing any code. You need both — frontend talks to Hawcx, backend verifies the result.

Frontend — create .env in your app root:

VITE_HAWCX_CONFIG_ID=your_config_id

Step 3: Frontend — Minimal Login

By default, start(email) auto-detects whether the user is new or returning.

src/App.tsx

import { HawcxProvider } from '@hawcx/react';
import { LoginFlow } from './LoginFlow';

export function App() {
  return (
    <HawcxProvider config={{
      configId: import.meta.env.VITE_HAWCX_CONFIG_ID
    }}>
      <LoginFlow />
    </HawcxProvider>
  );
}

src/LoginFlow.tsx

import { useState } from 'react';
import { useFlowState, useFlowActions } from '@hawcx/react';

export function LoginFlow() {
  const state = useFlowState();
  const { start, selectMethod, submitCode, reset } = useFlowActions();
  const [email, setEmail] = useState('');
  const [code, setCode] = useState('');

  if (state.status === 'idle') {
    return (
      <form onSubmit={(e) => { e.preventDefault(); start(email); }}>
        <input
          type="email"
          value={email}
          onChange={(e) => setEmail(e.target.value)}
          placeholder="Email"
        />
        <button type="submit">Continue</button>
      </form>
    );
  }

  if (state.status === 'loading') return <div>Loading...</div>;

  if (state.status === 'error') {
    return (
      <div>
        <p>Error: {state.error.message}</p>
        <button onClick={reset}>Try Again</button>
      </div>
    );
  }

  if (state.status === 'completed') {
    fetch('/exchange', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({
        authCode: state.authCode,
        codeVerifier: state.codeVerifier
      })
    }).then(() => window.location.href = '/');
    return <div>Success! Redirecting...</div>;
  }

  if (state.status === 'prompt') {
    const { prompt } = state;

    if (prompt.type === 'select_method') {
      return (
        <div>
          <h3>Choose method</h3>
          {prompt.methods.map((m) => (
            <button key={m.name} onClick={() => selectMethod(m.name)}>
              {m.label}
            </button>
          ))}
        </div>
      );
    }

    if (prompt.type === 'enter_code') {
      return (
        <form onSubmit={(e) => { e.preventDefault(); submitCode(code); }}>
          <p>Code sent to {prompt.destination}</p>
          <input
            value={code}
            onChange={(e) => setCode(e.target.value)}
            maxLength={prompt.codeLength}
          />
          <button type="submit">Verify</button>
        </form>
      );
    }

    return <div>Step: {prompt.type}</div>;
  }

  return null;
}

src/main.ts (replace placeholders with your env values)

import { createHawcxClient } from '@hawcx/core';

// Replace with your actual values from Step 2
const client = createHawcxClient({
  configId: 'your_config_id'
});

// Subscribe to state changes
client.onStateChange(renderUI);

// Start login
function login(email: string) {
  client.start(email);
}

function renderUI(state) {
  const app = document.getElementById('app')!;

  if (state.status === 'idle') {
    app.innerHTML = `
      <input type="email" id="email" placeholder="Enter your email">
      <button onclick="login(document.getElementById('email').value)">Continue</button>
    `;
    return;
  }

  if (state.status === 'loading') {
    app.innerHTML = '<div>Loading...</div>';
    return;
  }

  if (state.status === 'error') {
    app.innerHTML = `
      <p>Error: ${state.error.message}</p>
      <button onclick="client.reset()">Try Again</button>
    `;
    return;
  }

  if (state.status === 'completed') {
    fetch('/exchange', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({
        authCode: state.authCode,
        codeVerifier: state.codeVerifier
      })
    }).then(() => window.location.href = '/');
    app.innerHTML = '<div>Success! Redirecting...</div>';
    return;
  }

  if (state.status === 'prompt') {
    const { prompt } = state;

    if (prompt.type === 'select_method') {
      app.innerHTML = `
        <h3>Choose verification method</h3>
        ${prompt.methods.map(m =>
          `<button onclick="client.selectMethod('${m.name}')">${m.label}</button>`
        ).join('')}
      `;
      return;
    }

    if (prompt.type === 'enter_code') {
      app.innerHTML = `
        <p>Code sent to ${prompt.destination}</p>
        <input id="code" maxlength="${prompt.codeLength}" placeholder="Enter code">
        <button onclick="client.submitCode(document.getElementById('code').value)">Verify</button>
      `;
      return;
    }

    app.innerHTML = `<div>Step: ${prompt.type}</div>`;
  }
}

// Make login available globally for onclick
(window as any).login = login;
(window as any).client = client;

Step 4: Backend — Exchange Code

When the frontend completes, it sends authCode + codeVerifier. Exchange them for verified user claims in your backend using the Hawcx backend SDK. The route itself is yours to implement.

Backend — create .env in your server root:

# Config ID (public identifier)
HAWCX_API_KEY=your_config_id
# Optional (backend-only): credential blob for MFA / step-up management
# HAWCX_SECRET_KEY=hwx_sk_v1_...

Only the backend ever sees secrets. HAWCX_SECRET_KEY is used for delegation/step-up calls and is not required for the code exchange in this quickstart.

src/routes/auth.ts

import express from 'express';
import { HawcxOAuth } from '@hawcx/oauth-client';

const router = express.Router();

const oauth = new HawcxOAuth({
  configId: process.env.HAWCX_API_KEY!
});

// Your backend route (Hawcx does not provide this endpoint)
router.post('/exchange', async (req, res) => {
  try {
    const { authCode, codeVerifier } = req.body;
    const { claims } = await oauth.exchangeCode(authCode, codeVerifier);

    // claims.sub = user ID, claims.email = verified email
    // Create your own session here
    res.json({ success: true, userId: claims.sub, email: claims.email });
  } catch (error) {
    console.error('Auth failed:', error);
    res.status(401).json({ error: 'Authentication failed' });
  }
});

export default router;

routes/auth.py

from flask import Blueprint, request, jsonify
from hawcx_oauth_client import HawcxOAuth
import os

auth_bp = Blueprint('auth', __name__)

oauth = HawcxOAuth(
    config_id=os.getenv('HAWCX_API_KEY')
)

@auth_bp.route('/exchange', methods=['POST'])
def auth():
    try:
        data = request.json
        result = oauth.exchange_code(data['authCode'], data['codeVerifier'])

        # result.claims['sub'] = user ID, result.claims['email'] = verified email
        # Create your own session here
        return jsonify({'success': True, 'userId': result.claims['sub']})
    except Exception as e:
        print(f'Auth failed: {e}')
        return jsonify({'error': 'Authentication failed'}), 401

Optional (advanced): If you need backend-driven MFA setup or step-up verification, initialize the delegation client with HAWCX_SECRET_KEY (a credential blob from the dashboard). This is separate from the exchange above.


Step 5: Run & Verify

  1. Start your backend server.
  2. Start your frontend app.
  3. Enter email → select method → enter code.
  4. Confirm the backend receives authCode + codeVerifier and returns a success response.
  5. You should land on your homepage.

Next Steps