/ 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, you’re 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.