StealthQL

/ accept-invite snippets

Drop-in /auth/accept handler.

When you invite a member from the dashboard, we generate an accept URL pointing at https://your-app.com/auth/accept?invite=u_inv_.... That URL has to be served by your app — pick the snippet for your stack, drop it in, youre done.

Next.js (App Router)

File: app/auth/accept/route.ts

import { type NextRequest, NextResponse } from "next/server";

const STEALTHQL_URL = process.env.STEALTHQL_URL!;

/**
 * GET /auth/accept?invite=u_inv_...
 *
 * Validates the invite with the StealthQL runtime, mints a session,
 * sets the cookie, sends the user to the signed-in home.
 */
export async function GET(request: NextRequest) {
  const inviteId = request.nextUrl.searchParams.get("invite");
  if (!inviteId) {
    return NextResponse.redirect(new URL("/login?invite_error=missing", request.url));
  }

  const response = await fetch(`${STEALTHQL_URL}/v1/auth/accept-invite`, {
    method: "POST",
    headers: { "content-type": "application/json" },
    body: JSON.stringify({ inviteId }),
    cache: "no-store",
  });

  if (!response.ok) {
    return NextResponse.redirect(new URL("/login?invite_error=expired", request.url));
  }

  const { sessionToken } = await response.json();
  const redirect = NextResponse.redirect(new URL("/", request.url));
  redirect.cookies.set("session", sessionToken, {
    httpOnly: true,
    secure: process.env.NODE_ENV === "production",
    sameSite: "lax",
    path: "/",
    maxAge: 60 * 60 * 24 * 7,
  });
  return redirect;
}

Next.js (Pages Router)

File: pages/auth/accept.ts

import type { NextApiRequest, NextApiResponse } from "next";
import { serialize } from "cookie";

const STEALTHQL_URL = process.env.STEALTHQL_URL!;

export default async function handler(req: NextApiRequest, res: NextApiResponse) {
  const inviteId = String(req.query.invite ?? "");
  if (!inviteId) {
    res.redirect("/login?invite_error=missing");
    return;
  }

  const response = await fetch(`${STEALTHQL_URL}/v1/auth/accept-invite`, {
    method: "POST",
    headers: { "content-type": "application/json" },
    body: JSON.stringify({ inviteId }),
  });

  if (!response.ok) {
    res.redirect("/login?invite_error=expired");
    return;
  }

  const { sessionToken } = await response.json();
  res.setHeader(
    "set-cookie",
    serialize("session", sessionToken, {
      httpOnly: true,
      secure: process.env.NODE_ENV === "production",
      sameSite: "lax",
      path: "/",
      maxAge: 60 * 60 * 24 * 7,
    }),
  );
  res.redirect("/");
}

Express

Mount: app.get("/auth/accept", ...)

import type { Request, Response } from "express";

const STEALTHQL_URL = process.env.STEALTHQL_URL!;

export async function acceptInviteHandler(req: Request, res: Response) {
  const inviteId = String(req.query.invite ?? "");
  if (!inviteId) {
    return res.redirect("/login?invite_error=missing");
  }

  const response = await fetch(`${STEALTHQL_URL}/v1/auth/accept-invite`, {
    method: "POST",
    headers: { "content-type": "application/json" },
    body: JSON.stringify({ inviteId }),
  });

  if (!response.ok) {
    return res.redirect("/login?invite_error=expired");
  }

  const { sessionToken } = await response.json();
  res.cookie("session", sessionToken, {
    httpOnly: true,
    secure: process.env.NODE_ENV === "production",
    sameSite: "lax",
    path: "/",
    maxAge: 7 * 24 * 60 * 60 * 1000,
  });
  res.redirect("/");
}

// app.get("/auth/accept", acceptInviteHandler);

FastAPI

File: auth/accept.py

import os
import httpx
from fastapi import APIRouter, Request
from fastapi.responses import RedirectResponse

STEALTHQL_URL = os.environ["STEALTHQL_URL"]
router = APIRouter()

@router.get("/auth/accept")
async def accept_invite(request: Request):
    invite_id = request.query_params.get("invite")
    if not invite_id:
        return RedirectResponse(url="/login?invite_error=missing", status_code=302)

    async with httpx.AsyncClient() as client:
        response = await client.post(
            f"{STEALTHQL_URL}/v1/auth/accept-invite",
            json={"inviteId": invite_id},
        )

    if response.status_code != 200:
        return RedirectResponse(url="/login?invite_error=expired", status_code=302)

    data = response.json()
    redirect = RedirectResponse(url="/", status_code=302)
    redirect.set_cookie(
        key="session",
        value=data["sessionToken"],
        httponly=True,
        secure=os.environ.get("ENV") == "production",
        samesite="lax",
        path="/",
        max_age=7 * 24 * 60 * 60,
    )
    return redirect

# app.include_router(router)

Hono

Mount: app.get("/auth/accept", ...)

import { Hono } from "hono";
import { setCookie } from "hono/cookie";

const STEALTHQL_URL = process.env.STEALTHQL_URL!;
const app = new Hono();

app.get("/auth/accept", async (c) => {
  const inviteId = c.req.query("invite");
  if (!inviteId) {
    return c.redirect("/login?invite_error=missing");
  }

  const response = await fetch(`${STEALTHQL_URL}/v1/auth/accept-invite`, {
    method: "POST",
    headers: { "content-type": "application/json" },
    body: JSON.stringify({ inviteId }),
  });

  if (!response.ok) {
    return c.redirect("/login?invite_error=expired");
  }

  const { sessionToken } = await response.json();
  setCookie(c, "session", sessionToken, {
    httpOnly: true,
    secure: process.env.NODE_ENV === "production",
    sameSite: "Lax",
    path: "/",
    maxAge: 7 * 24 * 60 * 60,
  });
  return c.redirect("/");
});

export default app;

What the StealthQL runtime expects

Each snippet calls POST /v1/auth/accept-invite on your StealthQL runtime with the invite id. The runtime validates the id, flips the member to active, returns a session token, and (optionally) the email of the new member so you can redirect them to a personalized landing page. All five snippets share the same response shape — only the wrapper around the HTTP call changes.

If the invite is unknown, expired, or already used, the runtime returns 410 Gone. Each snippet redirects to /login?invite_error=expired in that case so your /login page can show a friendly message.