---
title: "Share anything"
description: "Share a record, a file or a screen as a private, expiring getqr.id link and QR code, print QR codes for posters, count opens with consent, send invite, deep and sign-in links, and do it all offline with the local driver."
section: "guides"
status: "Building for 0.3"
spec: "files-share"
canonical: "https://mainsdk.dev/docs/guides/share-anything"
updated: "2026-10-28"
---

# Share anything

> Share a record, a file or a screen as a private, expiring getqr.id link and QR code, print QR codes for posters, count opens with consent, send invite, deep and sign-in links, and do it all offline with the local driver.

Status: Building for 0.3 · Canonical: https://mainsdk.dev/docs/guides/share-anything · Updated: 2026-10-28

You will add sharing to Pinecart, a Next.js app on MainSDK: a customer shares an order as a link and QR code, the shop prints QR codes for its counter and flyers, and the team sends invite and sign-in links. Every link goes through one call, `share`, and lands on getqr.id, so it expires, can be revoked, carries nothing personal in its URL and shows "Powered by getqr.id". You build it offline with the local driver, switch to getqr.id for production, and finish with a passing `mainsdk check`. It takes about 30 minutes.

> **Note:** `share` (spec `files-share`, ADR-0056) and `links` (spec `links`) run from the repository on the `local` and `getqr` drivers. Nothing is published to npm yet. Steps that need other interfaces that are not built (`privacy` consent, sending email or WhatsApp) say so.

## You need

- A Next.js App Router app with a `main.yaml`, `auth` and `db` set up (see [Quickstart](https://mainsdk.dev/docs/getting-started/quickstart)).
- For production only: a getqr.id account and a `gq_live_` secret key, stored as a reference in your secrets manager (see [Secrets](https://mainsdk.dev/docs/interfaces/secrets)). A `gq_test_` key uses the getqr.id sandbox, where links always expire in 24 hours.

## Why one call

`share` is the only way a MainSDK app hands out a link, QR code or share page (ADR-0056). `auth` invites, `files`, `feeds`, `imports` reports, the seal record page and engagement messages all call it inside the SDK, so the same rules hold everywhere. `mainsdk check` fails on any other shortener or QR library (`MAIN-SHARE-0001`) and on calls to their hosts (`MAIN-SHARE-0007`).

## 1. Declare share in main.yaml

Use the local driver while you build. It mints links on your dev server, keeps them in process and needs no account or network.

```yaml title="main.yaml"
share:
  driver: local
  default-expiry: 7d
  max-expiry: 30d
```

For production, point an overlay at getqr.id. The key is a `secret:` reference; a literal fails with `MAIN-CONFIG-0004`.

```yaml title="main.env.prod.yaml"
share:
  driver: getqr
  key: secret:GETQR_API_KEY
```

The local driver refuses to start in production (`MAIN-SHARE-0008`), and `mainsdk check --env prod` reports `MAIN-CHECK-0026` if the overlay is missing.

## 2. Share a record

Share a read-only page of the record, never the record's data. The page is your own route; the link to it is a share.

```ts title="app/api/orders/[id]/share/route.ts"
import { auth, models, s, share } from "mainsdk";

const { orders } = models({ orders: s.object({ item: s.string(), total: s.int() }) });

export async function POST(req: Request, { params }: { params: Promise<{ id: string }> }) {
  const { id } = await params;
  const session = await auth.require(req);
  const order = await orders.for(session.user).get(id);
  const link = await share({ url: `https://pinecart.in/orders/${order.id}/view`, title: "Your Pinecart order" }, { expiresIn: "7d" });
  return Response.json(link); // { url, qr, id, expiresAt }
}
```

The URL holds nothing from the order. A `title` or `metadata` with an email or phone number is refused with `MAIN-SHARE-0013`, because getqr.id shows it in lists and webhooks. Share links are read-only: asking for any other access fails with `MAIN-SHARE-0003`.

To share an uploaded file, pass the `FileRef` from [files](https://mainsdk.dev/docs/interfaces/files) directly: `share(fileRef, { expiresIn: "1d" })`.

## 3. Show the QR code and a story image

```ts title="app/api/shares/[id]/qr/route.ts"
import { share } from "mainsdk";

export async function GET(_req: Request, { params }: { params: Promise<{ id: string }> }) {
  const { id } = await params;
  const qr = await share.qr(id, { format: "png", size: 1024 });
  return new Response(qr.bytes, { headers: { "content-type": qr.type } });
}
```

`share.story(id)` returns a 1080×1920 image with the QR, title and link, sized for Instagram and WhatsApp Status. QR codes are always dark on white with a quiet zone, so they scan; MainSDK never draws one itself. In the browser, `<QRCode/>` and `<ShareBar/>` from `@getqr/react` add the caption and "Powered by getqr.id".

## 4. List, change and revoke

```ts title="app/shares/actions.ts"
"use server";
import { share } from "mainsdk";

export async function myShares(cursor?: string) {
  return share.list({ status: "live", cursor, limit: 20 }); // only this tenant's shares
}

export async function rename(id: string) {
  return share.update(id, { title: "Order 1042", extend: "24h" }); // same link, same QR
}

export async function stop(id: string) {
  await share.revoke(id); // the link stops opening
}
```

An `extend` past `max-expiry` fails with `MAIN-SHARE-0002`. Share links always expire; asking to keep one forever fails with `MAIN-SHARE-0010`.

## 5. Count opens, with consent

Counts are plain numbers from getqr.id's own domain (time, country, device type, placement), never anything about the person who opened the link. Read them only after the person who shared has agreed.

```ts title="app/shares/stats.ts"
import { share } from "mainsdk";

// `analyticsConsent` is the person's recorded analytics consent from your own consent store.
export async function opens(id: string, analyticsConsent: boolean) {
  return share.stats(id, { consent: analyticsConsent, range: "30d" }); // MAIN-SHARE-0004 without consent
}
```

`share.usage()` returns this billing period's shares, opens and storage against your getqr.id plan, and logs a warning once when a meter passes its alert level.

## 6. Print QR codes for posters and counters

A campaign holds one kept link and QR code per placement: a flyer, a counter card, a newspaper insert. Placements are the only links that never expire, and they point at a public page or a WhatsApp chat, never at a record.

```ts title="app/marketing/diwali.ts"
import { share } from "mainsdk";

export async function diwaliCampaign() {
  const campaign = await share.campaigns.create({ name: "Diwali boxes", handle: "pinecart" });
  const { placements, printSheet } = await share.placements.create(campaign.id, [
    { slug: "counter", name: "Shop counter", medium: "counter", kind: "link", url: "https://pinecart.in/diwali" },
    { slug: "flyer-kothrud", name: "Kothrud flyers", medium: "print", kind: "link", url: "https://pinecart.in/diwali" },
  ]);
  return { placements, printSheet }; // printSheet: the URL of a printable sheet of the QR codes
}
```

If any row fails getqr.id's checks, nothing is created and you get `MAIN-SHARE-0011` with each row listed. Read scan counts with `share.placements.list(campaign.id, { consent })`, with the campaign owner's consent, as in step 5.

## 7. Receive getqr.id events

Declare the inbound endpoint and register it once.

```yaml title="main.yaml"
webhooks:
  inbound:
    getqr: { preset: getqr, secret: secret:GETQR_WEBHOOK_SECRET, path: /hooks/getqr }
share:
  webhooks:
    events: [share.opened, share.removed, upload.scanned, placement.scanned]
  track: { analytics: true }
```

```bash
npx mainsdk share webhooks add https://pinecart.in/hooks/getqr
```

It prints the signing secret once; store it as `GETQR_WEBHOOK_SECRET`. Then handle events:

```ts title="app/hooks/getqr.ts"
import { share, webhooks } from "mainsdk";

// Route handler for /hooks/getqr: the declared endpoint verifies the signature, then share.on handlers run.
export async function POST(req: Request) {
  return (await webhooks.handle(req)) ?? new Response("Not found", { status: 404 });
}

share.on("share.removed", async (event) => {
  console.info("share removed", event.id);
});
```

Each delivery is verified before your handler runs (a bad signature is refused with a 400 problem), runs once even if getqr.id retries, and runs in the tenant that made the share. `upload.scanned` marks a getqr.id upload ready without polling. Opens of shares made with `track: true` feed the `share.opens` metric with no person attached.

## 8. Send invite, deep and sign-in links

`links` builds on `share`. Declare your routes once.

```yaml title="main.yaml"
links:
  base: https://pinecart.in
  routes:
    order: { web: "/orders/:id", app: "order/:id", params: [id] }
    list: { web: "/lists/:id", app: "list/:id", params: [id] }
auth:
  methods: [passkeys, link]
```

```ts title="app/links/actions.ts"
"use server";
import { links } from "mainsdk";

export async function orderLink(orderId: string) {
  return links.create({ screen: "order", params: { id: orderId } }); // opens the app when installed
}

export async function inviteToList(listId: string, email: string) {
  return links.invite({ screen: "list", params: { id: listId } }, { email, uses: 1, expiresIn: "7d" });
}

export async function emailSignIn(email: string) {
  return links.signIn({ email }, { next: { screen: "order", params: { id: "latest" } } }); // { sent: true }
}
```

A deep link grants nothing: the person still signs in and passes `access`. An invite accepted with another email fails with `MAIN-LINK-0001` and stays usable. A sign-in link is never returned to your code, and the answer is the same whether or not the address has an account. `links.signIn` and `links.accept` need `auth` to provide its link method and sessions, and delivery by email or WhatsApp needs a sender; until those interfaces are built they fail with `MAIN-LINK-0005` and `MAIN-AUTH-0001`. With `links.apps` set, `links.route(req)` serves the Universal Links and App Links files; call it from a route handler (see [App links](https://mainsdk.dev/docs/interfaces/links)).

## 9. Test it offline

```ts title="app/shares/share.test.ts"
import assert from "node:assert/strict";
import { describe, it } from "node:test";
import { createTestApp } from "@mainsdk/testing";
import { share } from "mainsdk";

describe("sharing an order", () => {
  it("expires after 7 days", async () => {
    const testApp = createTestApp({ main: "./main.yaml" }); // share.driver: local
    const link = await share({ url: "https://pinecart.in/orders/ord_1/view" }, { expiresIn: "7d" });
    assert.ok(link.url.includes("/_mainsdk/s/"));
    testApp.clock.advance("8d");
    const [found] = (await share.list({ status: "expired" })).items;
    assert.equal(found?.id, link.id);
  });
});
```

## Verify

```bash
npx mainsdk check
npx mainsdk check --env prod
```

Both pass with no `MAIN-SHARE-` or `MAIN-LINK-` finding. Revoke a share link and open it again: it no longer opens (on the local driver it answers 410 with `MAIN-SHARE-0009`).

## If it fails

| Code | What happened | Fix |
| --- | --- | --- |
| `MAIN-SHARE-0001` | A shortener or QR library is imported in app code | Remove it and use `share()` or `links` |
| `MAIN-SHARE-0007` | Code calls a shortener or QR service, or `network.allow` lists one | Remove the call and the entry |
| `MAIN-SHARE-0004` | Counts were read without consent | Pass the person's `analytics` consent |
| `MAIN-SHARE-0008` | The local driver in production | Add the `main.env.prod.yaml` overlay with `driver: getqr` |
| `MAIN-SHARE-0013` | Personal data in a title or metadata | Use record ids your app resolves after sign-in |
| `MAIN-LINK-0002` | A screen or param isn't declared | Add it under `links.routes` |
| `MAIN-LINK-0005` | `links.signIn` without the `link` method | Add `link` to `auth.methods` |

## For agents

```text
Add sharing to this app with MainSDK. Use share() for every link, QR code and share page, and links for invite,
deep and sign-in links; never add a shortener or QR library. Use share.driver: local in main.yaml and getqr with
secret:GETQR_API_KEY in main.env.prod.yaml. Share read-only pages of records, never their data, and keep emails and
phone numbers out of titles and metadata. Read counts only with the person's analytics consent. Finish with a
passing npx mainsdk check and npx mainsdk check --env prod.
```

## Next

- [Share links and QR codes](https://mainsdk.dev/docs/interfaces/share): every call and option.
- [Invite, deep and sign-in links](https://mainsdk.dev/docs/interfaces/links): routes and association files.
- [Upload files](https://mainsdk.dev/docs/guides/file-uploads): the files a share can point at.
- [getqr.id](https://mainsdk.dev/docs/integrations/getqr-id): keys, sandbox and plans.
