Constaia
Integrations

Electron

Integrate Constaia into an Electron app without bundling the key: the renderer asks over IPC and the main process uploads to your backend.

Esta página ainda não está traduzida para o seu idioma. Mostramos a versão em inglês.

An Electron app is a client: it is installed on the user's computer, just like a website or a mobile app. So the Constaia key cannot live inside it, not even in the main process. The right flow:

  1. The renderer asks the main process, over IPC, to validate a document.
  2. The main process opens the file dialog, reads the file and uploads it to your backend with the user's session token.
  3. Your backend calls Constaia with the key and returns the verdict.

Why the key cannot ship in the app

  • The package can be opened. app.asar is an unencrypted archive; anyone can extract it with npx @electron/asar extract. Obfuscating the code or injecting the key through build-time environment variables changes nothing: it ends up in the binary.
  • A leaked key opens your whole account. It spends your credits and can read the stored analyses of all your users with GET /v1/analyses, extracted data included.
  • It cannot be revoked for a single user. You would have to rotate it and ship a new app version.
  • The SDK blocks it in the renderer: in a context with window and document, new Constaia() with a ck_ key throws secret_key_in_browser.

Coming soon: publishable keys

Publishable (client-safe) keys and verification links (POST /v1/verification-links) are not available yet. Today, every Constaia call comes from your server.

Backend

You need a route such as POST /api/verify-dni that requires the user's session, decides expect and checks and calls Constaia. Follow the guide for your stack: Node.js, Express, Fastify, NestJS, Hono or AWS Lambda. The key (CONSTAIA_API_KEY) and the webhook secret (CONSTAIA_WEBHOOK_SECRET) live only there.

Main process

src/main.ts
import { app, BrowserWindow, dialog, ipcMain } from "electron";
import { readFile } from "node:fs/promises";
import { basename, extname, join } from "node:path";

// YOUR backend, never api.constaia.com.
const API_BASE = "https://api.your-app.com";
const MIME: Record<string, string> = {
  ".jpg": "image/jpeg",
  ".jpeg": "image/jpeg",
  ".png": "image/png",
  ".webp": "image/webp",
  ".heic": "image/heic",
  ".pdf": "application/pdf",
};

ipcMain.handle("document:verify", async (event, sessionToken: string) => {
  const win = BrowserWindow.fromWebContents(event.sender)!;
  const { canceled, filePaths } = await dialog.showOpenDialog(win, {
    properties: ["openFile"],
    filters: [{ name: "Documents", extensions: ["jpg", "jpeg", "png", "webp", "heic", "pdf"] }],
  });
  if (canceled || !filePaths[0]) return { canceled: true };

  const path = filePaths[0];
  const data = await readFile(path);
  if (data.byteLength > 20 * 1024 * 1024) {
    return { error: { code: "file_too_large", message: "20 MB maximum." } };
  }

  const form = new FormData();
  form.append("file", new Blob([data], { type: MIME[extname(path).toLowerCase()] }), basename(path));

  try {
    const res = await fetch(`${API_BASE}/api/verify-dni`, {
      method: "POST",
      headers: { authorization: `Bearer ${sessionToken}` },
      body: form,
      signal: AbortSignal.timeout(90_000),
    });
    const body = await res.json().catch(() => null);
    if (!res.ok) return { error: body?.error ?? { code: "http_error", message: `HTTP ${res.status}` } };
    return { result: body };
  } catch {
    return { error: { code: "network_error", message: "Cannot reach the server." } };
  }
});

function createWindow() {
  const win = new BrowserWindow({
    width: 900,
    height: 700,
    webPreferences: { preload: join(__dirname, "preload.js"), contextIsolation: true, sandbox: true },
  });
  win.loadFile("index.html");
}

app.whenReady().then(createWindow);
app.on("window-all-closed", () => {
  if (process.platform !== "darwin") app.quit();
});
  • The main process opens the dialog: the renderer cannot ask to upload an arbitrary path from disk.
  • basename(path) keeps the original name, which your backend forwards to Constaia (in test mode it decides the response).
  • The 90 s timeout covers a synchronous analysis of up to 30 s plus the upload.

Preload

src/preload.ts
import { contextBridge, ipcRenderer } from "electron";

contextBridge.exposeInMainWorld("documents", {
  verify: (sessionToken: string) => ipcRenderer.invoke("document:verify", sessionToken),
});

Only one specific function is exposed, not the whole ipcRenderer.

Renderer

src/renderer.ts
type Verdict = { status: "valid" | "invalid" | "review"; reasons: { code: string; message: string }[] };
type VerifyResponse =
  | { canceled: true }
  | { result: { id: string; status: string; verdict: Verdict | null; warnings: string[] } }
  | { error: { code: string; message: string } };

declare global {
  interface Window {
    documents: { verify(sessionToken: string): Promise<VerifyResponse> };
  }
}
declare function getSessionToken(): Promise<string>; // your login

const button = document.querySelector<HTMLButtonElement>("#verify")!;
const output = document.querySelector<HTMLDivElement>("#result")!;

button.addEventListener("click", async () => {
  button.disabled = true;
  output.textContent = "Analysing…";
  // YOUR backend's session token, obtained on your login screen.
  const response = await window.documents.verify(await getSessionToken());
  button.disabled = false;

  if ("canceled" in response) {
    output.textContent = "";
  } else if ("error" in response) {
    output.textContent = response.error.message;
  } else {
    const verdict = response.result.verdict;
    const labels = { valid: "Valid document", invalid: "Invalid document", review: "We will review it manually" };
    output.textContent = verdict
      ? `${labels[verdict.status]}. ${verdict.reasons.map((r) => r.message).join(" ")}`
      : "Analysis in progress; we will let you know when it finishes.";
  }
});

export {};

getSessionToken() is your authentication function. If you prefer a ready-made UI, the widget works inside the renderer: <constaia-upload endpoint="https://api.your-app.com/api/verify-dni"> with the headers attribute for the token or with-credentials for cookies. Your backend must allow CORS from the app's origin.

Test in test mode

  1. Start your backend with CONSTAIA_API_KEY=ck_test_....
  2. In the app, click the button and pick a file named dni_valid.jpg: you will see "Valid document".
  3. Repeat with dni_expired.jpg (invalid, reason not_expired) and blurry.jpg (review, reason low_quality).

Any real JPEG or PNG, renamed, will do. More names in Test mode.

Errors

Your backend maps SDK errors (no credits, rate limit, unreadable file…) to HTTP responses with { "error": { "code", "message" } }. The main process passes them unchanged to the renderer, which shows message. Codes are listed in Errors.

Production checklist

  • No ck_ key in the code, in bundled .env files or in app.asar (search for it before releasing).
  • The backend route requires a session and applies per-user rate limiting.
  • contextIsolation: true, sandbox: true and a preload that exposes only what is needed.
  • 20 MB limit in the main process and the backend; timeouts of 60 s or more along the whole path.
  • The backend returns only the verdict and reasons, not extracted data, unless the app needs it.

Next steps

Nesta página