MainSDK
Search⌘K
How-to

Link to a screen in your app

Declare your screens once, then send links that open the right screen in the app or on the web, invite people, and revoke a link.

Building for 0.4

Status Status is where the work is: Planned (in a milestone, not started), Building (tickets in progress), Preview (usable, may change), then the stability it ships at.

Stability The promise an interface ships with.

  • Stable Follows semantic versioning; breaking changes need a new major, 12 months of overlap and a codemod.
  • Beta Usable in production; may change before it becomes stable.
  • Experimental May change or go away; needs an explicit opt-in in main.yaml.
Stability levels
Updated 2026-10-28

You will declare one screen, make a link to it, resolve it and revoke it. It takes about 15 minutes. Every link is a getqr.id share, so it expires and shows nothing of the screen or its params.

You need

1. Declare the screens

main.yaml
links:
  base: https://app.example.com
  routes:
    order: { web: /orders/:id, app: orders/:id, params: [id] }
app/orders/actions.ts
import { links } from "mainsdk";

export async function orderLink(orderId: string) {
  const link = await links.create({ screen: "order", params: { id: orderId } }, { expiresIn: "14d" });
  return link.url;
}

A screen you did not declare, or different params, fail with MAIN-LINK-0002.

app/open/route.ts
import { links } from "mainsdk";

export async function GET(req: Request) {
  const target = await links.resolve(new URL(req.url).searchParams.get("u") ?? "");
  return Response.json(target);   // { kind, route, valid, reason? }
}

resolve never throws for an unknown URL. valid is false with reason expired, revoked, used-up or unknown.

4. Invite someone

app/lists/[id]/invite/route.ts
import { links } from "mainsdk";

export async function inviteJane(id: string) {
  return links.invite(
    { screen: "list", params: { id } },
    { email: "jane@example.com", uses: 1, expiresIn: "7d" },
  );
}

The person accepts with links.accept(req, token) after signing in.

5. Revoke

app/orders/revoke.ts
import { links } from "mainsdk";

export async function revoke(id: string) {
  await links.revoke(id);
}

Check it worked

app/orders/links.test.ts
import { it } from "node:test";
import assert from "node:assert/strict";
import { createTestApp } from "@mainsdk/testing";
import { links } from "mainsdk";

it("links AC3 reports a revoked link", async () => {
  createTestApp({ main: "./main.yaml" });
  const link = await links.create({ screen: "order", params: { id: "ord_1" } });
  await links.revoke(link.id);
  assert.partialDeepStrictEqual(await links.resolve(link.url), { valid: false, reason: "revoked" });
});

Troubleshooting

  • MAIN-LINK-0001 (410): the invite is used up, expired, revoked or for another email.
  • MAIN-LINK-0003: a route param identifies a person, such as email. Pass a record id instead.
  • MAIN-LINK-0004: links.apps is missing what the association files need, or links.base is not HTTPS.

See the links interface.

Was this page helpful?

↑↓ moveEnter openEsc close

Not decided yet