MainSDK
Search⌘K
How-to

Debug your app

Go from an error to a fix with local logs, error codes and mainsdk logs, then debug production with live tail, debug sessions, replay and triage. Explain, driver tracing and OpenTelemetry export are planned.

Building for 0.6

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

Find out why a request failed and fix it, using only local tools. Every error and log MainSDK writes carries a stable code, the file and line in your code, the main.yaml rule involved and a fix, so you or your coding agent can act on it directly.

Before you start: a MainSDK app that runs locally with mainsdk dev. Steps 1 to 4 are free and local; nothing leaves your machine.

1. Find the error in the local logs

By default records print to the terminal. To keep them for mainsdk logs, add a file sink once; it writes JSON lines to .mainsdk/events/ (git-ignored). Passing sinks replaces the terminal output.

server/index.ts
import { configure, fileSink } from "mainsdk";

configure({ sinks: [fileSink(".mainsdk/events/app.jsonl")] });
bash
npx mainsdk logs --level error
text
2026-11-12T10:04:31.208Z ERROR This record belongs to another owner

To watch new events, or to see every record from one request (records carry attributes.trace_id inside a request with a trace):

bash
npx mainsdk logs --follow
npx mainsdk logs --trace 4bf92f3577b34da6a3ce929d0e0e4736 --json

--json prints the stored record, which includes the code and fix of a MainError.

2. Look up the code

Find the code in the error codes reference for its cause and fix; the fix is also in the record from step 1. A problem response carries the same code and fix for your client. mainsdk explain (offline lookup by code or trace id) is planned.

text
MAIN-DATA-0007 · This record belongs to another owner · 403
fix: Read orders through db.orders.for(session.user) so users only reach their own.

Your coding agent reads the same codes and fixes; see Use MainSDK with your coding agent.

3. Fix the code

MAIN-DATA-0007 means the owner scope did its job: the order route refused a record that belongs to someone else. The question is why the user asked for it. Follow the trace back to the list endpoint, which still reads orders through the escape hatch, so it returns every user's orders:

app/api/orders/route.ts (before)
import { auth } from "mainsdk";

declare const sql: { query(text: string): Promise<unknown[]> };   // your own SQL client

export async function GET(req: Request) {
  await auth.require(req);
  const orders = await sql.query("select * from orders limit 20");   // no owner scope
  return Response.json(orders);
}

Read through the scoped model instead, so the list only holds this user's orders:

app/api/orders/route.ts (after)
import { auth, models, s } from "mainsdk";

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

export async function GET(req: Request) {
  const session = await auth.require(req);
  const page = await orders.for(session.user).list({ limit: 20 });
  return Response.json(page);
}

A query through models without .for(user) does not compile, so look for other places that read orders through your own SQL client.

4. Look at the app's state (when the cause is a driver or a dependency)

bash
npx mainsdk info
npx mainsdk status

info lists each interface with its driver, the network destinations and the health and resource settings. status shows dependencies, which are critical, and resource pressure. Tracing each provider call with MAINSDK_DEBUG=driver is planned.

5. Verify the fix

Repeat what the user did while following the error log:

bash
npx mainsdk logs --level error --follow

Open the order list again: it shows only your own orders, and no new MAIN-DATA-0007 line appears. Add a test so it stays fixed; see Test your app.

Send telemetry to your own OpenTelemetry backend

Planned: OTLP export of logs, traces and metrics to any endpoint, redacted in process, through a telemetry block in main.yaml. It is not built, and main.yaml has no telemetry key yet. Until then, give configure() a sink of your own: a Sink is a function (kind, record) => void that receives each redacted record.

Debug production with MainSDK Cloud

Send logs to the Cloud with the sink from mainsdk/feedback. It sends nothing until you are logged in and have set telemetry.to: [mainsdk] or MAINSDK_TELEMETRY=1, and it replaces emails, bearer tokens and key formats with [redacted] before anything leaves the process.

server/telemetry.ts
import { cloudSink } from "mainsdk/feedback";

declare const session: { exists(): boolean };
declare const config: { telemetry: { to: string[] } };
declare const client: { emit: Parameters<typeof cloudSink>[0]["emit"] };

export const sink = cloudSink({
  loggedIn: () => session.exists(),
  to: () => config.telemetry.to,
  flag: () => process.env.MAINSDK_TELEMETRY,
  emit: (event) => client.emit(event),
});
TaskCommandPlan
Live tail from production, for one user, trace or levelmainsdk logs --follow --env prod --user u_8f3Free; asks for a fresh passkey
Raise detail for one user, request or release, for up to 60 minutes; it switches itself offmainsdk debug start --user u_8f3 --for 30mTeams
End a debug session earlymainsdk debug stop <session-id>Teams
Replay a failed request locally on fake driversmainsdk replay 4bf92f3577b34da6a3ce929d0e0e4736Teams
Install the GitHub app for the projectmainsdk triage connect github --installation <id>Free
Group errors, find the release and commit, open one GitHub issue per groupmainsdk triage runTeams (hosted)
Open a draft fix pull request that waits for check --json to passmainsdk triage fix <issue> --head <branch>Free

A session names one user, one request or one release and lasts at most 60 minutes, so --for 90m runs for 60. The app reads the active sessions with its ingest key and keeps records below its log level only for the target, through the raise option of the log:

server/log.ts
import { Log } from "mainsdk";
import { raiseFor, type DebugSession } from "mainsdk/feedback";

declare const active: DebugSession[];   // the sessions your app read with its ingest key

export const log = new Log({ raise: raiseFor({ sessions: () => active, now: () => Date.now() }) });

Nothing is redeployed. A session ends itself on time, and its start and end are written to an audit record. Records stay redacted.

Replay needs a file, mainsdk.replay.ts, that exports a function for each action you want to replay. It runs the recorded input on the test app, where every interface is a fake, and any call that tries to reach the network is blocked and counted:

mainsdk.replay.ts
declare function refund(input: unknown, app: unknown): unknown;

export default {
  "orders.refund": (input: unknown, app: unknown) => refund(input, app),
};
bash
mainsdk replay 4bf92f3577b34da6a3ce929d0e0e4736
text
orders.refund: reproduced MAIN-DATA-0007; 0 outbound calls

On Free, mainsdk debug start and mainsdk replay get HTTP 402 from the Cloud with a link to Teams, and your app is unaffected. See Collect customer reports for the report button that arrives with its trace.

See where telemetry goes

bash
npx mainsdk telemetry status --json

It shows the destinations telemetry is sent to and whether MainSDK Cloud may receive it. Nothing is sent unless you declared it.

Next

Was this page helpful?

↑↓ moveEnter openEsc close

Not decided yet