# Custom Views

> Live, reactive dashboards authored as TSX and rendered natively in the Struere dashboard

Custom Views are live dashboards authored as TSX files in your project's `views/` directory. Each view is compiled by the CLI into a JavaScript bundle, stored in Convex, and rendered natively inside the Struere dashboard. Data updates flow through Convex's live subscriptions — there is no refresh button.

Use a custom view when the built-in data browser is not enough: cross-entity joins, KPIs, ranked lists, status breakdowns, or anything that needs a tailored layout. The built-in browser is still the right tool for routine CRUD on a single entity type.

## Authoring Surface

A view is a single file that exports one `defineView(...)` call:

```tsx
import { defineView, Card, KPI, Stack, useViewQuery } from "struere/view"

export default defineView({
  name: "Operations Dashboard",
  slug: "operations-dashboard",
  description: "Live overview of sessions and teachers.",
  queries: {
    sessions: { type: "session", limit: 100 },
  },
  render: () => {
    const sessions = useViewQuery("sessions")
    return (
      <Stack gap="md">
        <Card title="Sessions">
          <KPI label="Total" value={sessions.data?.length ?? 0} />
        </Card>
      </Stack>
    )
  },
})
```

The only allowed import specifier is `"struere/view"`. Everything you can render — layout primitives, KPIs, tables, status pills, hooks — comes from that one module.

## Lifecycle

```
views/foo.tsx
      │
      ▼
┌─────────────────────────────┐
│ 1. CLI compile              │
│    AST validator            │
│    (banned imports,         │
│     raw HTML, eval, ...)    │
│    esbuild bundle           │
│    SHA-256 hash             │
└──────────┬──────────────────┘
           │
           ▼
┌─────────────────────────────┐
│ 2. Sync to Convex           │
│    source + compiledModule  │
│    + queries + hash         │
│    (env-scoped)             │
└──────────┬──────────────────┘
           │
           ▼
┌─────────────────────────────┐
│ 3. Dashboard <ViewHost>     │
│    Subscribe to customViews │
│    Dynamic-import bundle    │
│    Mount in sandbox boundary│
└──────────┬──────────────────┘
           │
           ▼
┌─────────────────────────────┐
│ 4. useViewQuery resolves    │
│    Convex live subscription │
│    Permission check applies │
│    (scope + field masks)    │
└─────────────────────────────┘
```

## Sandboxing

The component library is the sandbox. Because the AST validator rejects raw HTML elements, banned globals, dynamic `import()`, timers, and `new Function`/`new Worker` at compile time, the bundle that reaches the dashboard cannot escape the component surface. No iframe is needed.

Server-side, every `useViewQuery` call routes through `runViewQuery`, which builds an `ActorContext`, runs `assertCanPerform("list", entityType)`, and applies scope filters and field masks before returning rows. A view never sees data the viewer is not allowed to read.

## Scoping

Views are environment-scoped via `customViews`. The slug must be unique per organization and environment, and must match `/^[a-z0-9][a-z0-9-]*$/`. `struere dev` syncs to `development` and `eval`; `struere deploy` syncs to `production`.

## Next Steps

- [defineView](../sdk/define-view) — full reference for the authoring API
- [View Components](../sdk/view-components) — every component, with prop signatures
- [View Data Hooks](../sdk/view-data-hooks) — `useViewQuery`, `useIndex`, and reactivity
- [Authoring a Custom View](../cli/views) — walkthrough from `struere add view` to the dashboard
- [Custom Views Compile Errors](../reference/view-compile-errors) — every validator message
