SIGIL · INTEGRATION DOCS← Back to site
[ GUIDES ]

Server-side integration

Use callback when your app has a server. Sigil POSTs the result from its Rust layer directly to your endpoint — the result never touches the browser URL bar, and your server can validate, store, and act on it atomically.

[ CALLBACK vs REDIRECT_URI ]

callback (this guide) is right for anything involving real money or server-managed sessions. redirect_uri is simpler but puts the result in the browser URL — see the React or Vanilla JS guides for that pattern.

Express.js

Setup

server.ts
import express from 'express';
import { parseCallbackResponse } from '@sigil-oss/connect';

const app = express();
app.use(express.json({ limit: '16kb' })); // Sigil result bodies are small

Callback endpoint

Sigil POSTs to this URL after the user approves or rejects. Always respond 200 once you've accepted the payload — Sigil will retry on non-2xx responses.

routes/sigil.ts
// POST /api/sigil/callback  — receives Sigil's HTTP POST after user acts
app.post('/api/sigil/callback', async (req, res) => {
  // 1. Parse and validate the shape
  let result;
  try {
    result = parseCallbackResponse(req.body);
  } catch {
    return res.status(400).json({ error: 'invalid_payload' });
  }

  // 2. Verify the nonce — match it against what you stored when building the request
  const pending = await db.pendingRequest.findUnique({ where: { nonce: result.nonce } });
  if (!pending) return res.status(400).json({ error: 'unknown_nonce' });
  await db.pendingRequest.delete({ where: { nonce: result.nonce } });

  // 3. Handle each outcome
  switch (result.status) {
    case 'connected':
      await db.session.upsert({
        where: { identity: result.identity },
        create: { identity: result.identity, permissions: result.permissions },
        update: { permissions: result.permissions },
      });
      break;

    case 'signed':
      if (result.type === 'transfer' || result.type === 'sc_call') {
        await db.transaction.create({
          data: {
            txHash: result.tx_hash,
            identity: result.identity,
            targetTick: result.target_tick,
          },
        });
      }
      break;

    case 'rejected':
      // result.reason === 'user_rejected'
      break;
  }

  res.sendStatus(200); // Sigil retries on non-2xx
});

Storing nonces before the request

Build the sigil:// URI server-side and persist the nonce so the callback handler can verify it. Never build the URI directly in the browser when using server callbacks — the client can't be trusted to generate nonces that your server hasn't seen.

routes/sigil.ts
// Before building the sigil:// URI, store the nonce server-side
// so the callback handler can verify it belongs to a real request.

import { buildSigilUrl, createEnvelope, createConnectRequest } from '@sigil-oss/connect';

app.get('/api/sigil/connect', async (req, res) => {
  const req_ = createConnectRequest({
    type: 'connect',
    dapp: { name: 'My App', origin: 'https://myapp.example' },
    permissions: ['transfer', 'sign_message'],
  });

  // Store nonce with TTL matching the request expiry
  await db.pendingRequest.create({
    data: { nonce: req_.nonce, expiresAt: new Date(req_.exp! * 1000) },
  });

  // Return the URI to the client — client opens it
  const url = buildSigilUrl(
    createEnvelope(req_, { callback: 'https://myapp.example/api/sigil/callback' })
  );
  res.json({ url });
});
[ ALWAYS VERIFY THE NONCE ]

A callback with an unknown nonce is either a replay or a request you didn't originate. Reject it before touching any application state. Delete the nonce from the store on first use so it can't be replayed.

Sign-in verification

For the sign-in pattern — user signs a message, your server verifies the signature against their Qubic public key. Use the @qubic-lib/crypto package or equivalent to verify the Qubic ECDSA signature.

routes/auth.ts
// routes/auth.ts
import { verifyQubicSignature } from '@qubic-lib/crypto';
import type { Express } from 'express';

export function registerAuthRoutes(app: Express) {
  // POST /api/auth/qubic — verify a sign_message result sent from the browser
  app.post('/api/auth/qubic', async (req, res) => {
    const { identity, signature, public_key, nonce, issuedAt } = req.body;

    // 1. Deduplicate nonce (ioredis example — adapt to your Redis client)
    const key = `sigil:nonce:${nonce}`;
    const used = await redis.get(key);
    if (used) return res.status(400).json({ error: 'nonce_reused' });
    await redis.set(key, '1', 'EX', 300);

    // 2. Reconstruct the exact message that was signed
    const message = [
      'Sign in to My App',
      `nonce: ${nonce}`,
      `issuedAt: ${issuedAt}`,
    ].join('\n');

    // 3. Verify the Qubic ECDSA signature
    const valid = await verifyQubicSignature({ message, signature, publicKey: public_key });
    if (!valid) return res.status(401).json({ error: 'invalid_signature' });

    // 4. Identity verified — issue a session token
    const token = await createSession(identity);
    res.json({ token });
  });
}

Next.js

Callback route — App Router

app/api/sigil/callback/route.ts
// app/api/sigil/callback/route.ts — Next.js App Router
import { parseCallbackResponse } from '@sigil-oss/connect';
import { NextResponse } from 'next/server';

export async function POST(request: Request) {
  const body = await request.json();

  let result;
  try {
    result = parseCallbackResponse(body);
  } catch {
    return NextResponse.json({ error: 'invalid_payload' }, { status: 400 });
  }

  // Verify nonce, handle result…
  switch (result.status) {
    case 'connected':
      // store session
      break;
    case 'signed':
      // record tx
      break;
    case 'rejected':
      break;
  }

  return new NextResponse(null, { status: 200 });
}

Sign-in route — App Router

app/api/auth/qubic/route.ts
// app/api/auth/qubic/route.ts — Next.js App Router
import { NextResponse } from 'next/server';

export async function POST(request: Request) {
  const { identity, signature, public_key, nonce, issuedAt } = await request.json();

  // Deduplicate nonce
  const key = `sigil:nonce:${nonce}`;
  if (await redis.get(key)) {
    return NextResponse.json({ error: 'nonce_reused' }, { status: 400 });
  }
  await redis.set(key, '1', { ex: 300 });

  // Reconstruct and verify
  const message = ['Sign in to My App', `nonce: ${nonce}`, `issuedAt: ${issuedAt}`].join('\n');
  const valid = await verifyQubicSignature({ message, signature, publicKey: public_key });
  if (!valid) return NextResponse.json({ error: 'invalid_signature' }, { status: 401 });

  const token = await createSession(identity);
  return NextResponse.json({ token });
}