Skip to content

About

No description, website, or topics provided.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Repository files navigation

anfra host example

A host for anfra Data Apps that you run yourself, in place of the appserve pages anfra serve has built in. It is a reference for putting Data Apps in a page of your own: everything it needs from anfra is the published @holistics/anfra-sdk and the core HTTP API.

  • A sidebar lists the repo's Data Apps; each opens at /apps/<path>.
  • The stage runs the open Data App in a sandboxed frame. Its queries come back to the page over the SDK's bridge and are answered by anfra through this host's server. So are its exports: a Data App's query.export() downloads a query's whole result as a CSV.
  • Pictures: the toolbar downloads a PNG or PDF of the whole app, a node's row in the Structure tab one of that container or block, and a Data App can save its own with Anfra.render(). The SDK draws them in the frame; this host turns that on (render: true in src/data-app.ts), since it has the page save a file the app's code made. Turn it on only for apps you trust with that.
  • A toolbar reloads the app, and opens the inspect panel:
    • Structure: the app's parts as its markup declares them, as a tree. Hover a node to highlight it on the page, click to scroll to it, copy its handle ([Revenue](data-anfra-block="revenue")) to point an agent at it, or download a picture of it. Pick mode goes from the page to the tree.
    • Data: every app's queries (state, rows, executed AQL and SQL), controls and selection.
  • Light or dark follows the system until you pick one with the toggle next to the title.
  • In development, the open app reloads when its file or the AML changes.

Tested with anfra 0.8.0 and @holistics/anfra-sdk 0.8.0. Bump them together: the SDK is released with anfra.

How it fits together

browser                          this host (Node, Hono)                 anfra serve --no-apps
┌──────────────────────┐        ┌────────────────────────────┐         ┌────────────────────┐
│ page (React)         │ /api   │ allowlist, rate limit,     │ /api    │ core API           │
│  └ frame: Data App ──┼─bridge─┤ "your auth goes here"  ────┼────────▶│ (no auth: private) │
│                      │ /host  │ apps/ read from ANFRA_REPO │         └────────────────────┘
└──────────────────────┘        └────────────────────────────┘
  • anfra answers the core API only. Run with --no-apps, it serves no pages, and it must listen where only this server can reach it: anfra serve has no authentication.
  • The server (server/) is the one origin the browser talks to:
    • /api/core.show, /api/core.query and /api/core.query.export, passed to anfra, and nothing else of the core API. Queries and exports are rate-limited per client, each with its own limit. The spot to check your own session is marked in server/anfra.ts.
    • /exports/download/<token>/<name>: an export's file, streamed from anfra. anfra makes its export links at this server's origin (ANFRA_SITE_URL, below), so the browser downloads through here, never from anfra directly. A link works for whoever has it until it expires; a host whose readers sign in checks the session here too (marked in server/anfra.ts).
    • /host/apps (the Data App tree, labelled by each file's <title>), /host/files/<path> (a definition or a file it refers to, sent sandboxed) and, in development, /host/events (live reload). The core API has nothing to list or read app files, so the server reads them from the repo's apps/ folder itself.
    • /health: this host's version and anfra's, or 503 while anfra does not answer.
    • With MCP=1, /mcp: anfra's MCP endpoint, passed through. See MCP.
    • In production, the built page, with each Data App's link preview.
  • The page (src/): every call into the SDK is in src/data-app.ts. That file is the part to copy into a host of your own; the rest is UI.

Run it

You need Node 22 or newer, pnpm, and anfra.

  1. Start anfra on the Data Folder, without its own pages:

    cd path/to/your/data-folder && anfra serve --no-apps

    Set ANFRA_SITE_URL to where the browser reaches this host, so the export links anfra makes point here: http://localhost:5173 in development. Without it, a link points at anfra's own address, which the browser can't reach.

    Or pnpm dev:anfra, which does the same in ANFRA_REPO, with ANFRA_SITE_URL at http://localhost:5173 unless you set it. With Docker:

    docker run --rm -p 127.0.0.1:7878:7878 -e ANFRA_SITE_URL=http://localhost:5173 -v "$PWD:/repo" ghcr.io/holistics/anfra:0.8.0 serve --no-apps --addr 0.0.0.0:7878
  2. Start the host:

    pnpm install
    ANFRA_REPO=path/to/your/data-folder pnpm dev

    Open http://localhost:5173. Vite serves the page with hot reload and passes /api, /host, /exports and /health to the server on port 8787.

.npmrc points the @holistics scope at npmjs, in case your own npm config sends it elsewhere.

Production

pnpm build
NODE_ENV=production ANFRA_REPO=… ANFRA_URL=… PORT=5173 HOSTNAME=0.0.0.0 pnpm start

Run anfra with ANFRA_SITE_URL at this host's public origin (https://data.example.com). One process serves the page, the API and the Data Apps. It reads the app tree once at startup and does not watch the repo. Built assets are cached for a year (their names are hashed); definitions for a minute, with an ETag.

Configuration

Variable Default
ANFRA_REPO ../demo-data The Data Folder anfra serves. Data Apps are its apps/**/*.html.
ANFRA_URL http://127.0.0.1:7878 Where anfra answers. Keep it off the public network.
PORT, HOSTNAME 8787, 127.0.0.1 Where this server listens.
NODE_ENV production serves the built page, without live reload.
QUERY_RATE_LIMIT 120 core.query requests a minute per client.
EXPORT_RATE_LIMIT 10 core.query.export requests a minute per client. Each runs its query to the last row.
MCP 1 passes anfra's MCP endpoint through at /mcp. See MCP.
TRUST_PROXY 1 takes the client's address from X-Forwarded-For: the last entry, which the one proxy in front of this server appends.
ANFRA_SITE_URL This host's public origin, for the absolute links in link previews. Without it, each request's origin (with X-Forwarded-Proto under TRUST_PROXY=1).

Link previews

A link to a Data App shared in Slack, a chat app or a social site shows the app's title, a description and an image. Their crawlers run no script, so in production the server writes the tags (og:*, twitter:card) into the page it sends for /apps/<path>, from the definition's own head:

  • the title: og:title, else <title>, else the file name;
  • the description: og:description, else <meta name="description">, else a line about Data Apps;
  • the image: og:image, a URL or a path relative to the definition, else public/og-image.png, the anfra card. A file under apps/ that isn't there shows the default (and is warned of at startup).

An image under apps/, and the default, gets ?v= and a short hash of its bytes, read at startup, so a crawler that caches images by URL (Facebook, LinkedIn) fetches a changed one. They are said to be 1200x630 (og:image:width, og:image:height), as no image is read to learn its size: pre-render them at that size in the Data Folder, as anfra-demo-data's scripts/og_snapshots.py does. Every image gets og:image:type from its extension (png, jpg, webp) and og:image:alt, the title.

<title>Funnel Analysis</title>
<meta name="description" content="A customer funnel over ecommerce orders, with breakdowns and drill-downs.">
<meta property="og:image" content="funnel-analysis.png">

Set ANFRA_SITE_URL to this host's public origin, as for anfra, so the links in the tags point there.

Folder and app metadata

A repo can say how its folders and Data Apps are shown in an optional apps/_meta.yaml (or _meta.yml). Keys are paths relative to apps/ (a Data App's with its .html), so renaming something here never changes a URL. Every field is optional; without the file, folders are named by their directory, apps by their <title>, and everything is open and alphabetical.

folders:
  sales/:
    name: Sales & Revenue   # shown in place of the directory name
    icon: wallet
    tags: [finance, weekly]
    collapsed: true         # starts closed (open anyway if it holds the open app)
    order: 1                # before the unordered, lowest first
apps:
  sales/overview.html:
    name: Revenue overview  # shown in place of the <title>
    icon: chart-line
    tags: [kpi]
    endorsed: true          # a green check on its icon
    order: 1
  • icon is one of the names in server/entry.ts (lucide, kebab-case); an unknown one is logged and falls back to the default.
  • endorsed: true (Data Apps) puts a small green check on the icon's bottom right.
  • The sidebar's filter matches names and tags.
  • order puts entries first, lowest first; the rest follow by file name.
  • A Data App's link preview takes its own og:title first, then name, then its <title>.
  • The server logs each problem once while it lasts: unknown fields, a field on the wrong kind (endorsed on a folder), a key written twice (sales and sales/), a key under the wrong section, and keys that match nothing.

MCP

With MCP=1, the server passes anfra's MCP endpoint (streamable HTTP) through at /mcp, so an agent can connect to https://<this host>/mcp. It is a plain passthrough, for demos: unlike /api, it has no allowlist, no rate limit and no auth spot. Every op anfra serves as a tool is reachable, except core.ingest, which rebuilds the search catalog and is refused here.

  • Run anfra without --no-mcp: anfra serve --no-apps serves /mcp.
  • core.query.export answers with a link to the file, downloaded through this server as a Data App's export is (/exports/download/*, served with or without MCP=1), so start anfra with ANFRA_SITE_URL set to this host's public URL, such as https://demo.anfra.ai.

Checks

pnpm typecheck
pnpm lint      # Biome; pnpm format to fix
pnpm build

Layout

server/
  main.ts      start: config, listen
  app.ts       the routes
  anfra.ts     /api: the allowlist, rate limits, auth spot; export downloads; /health
  mcp.ts       /mcp, passed to anfra (MCP=1)
  catalog.ts   the Data App tree
  files.ts     a file under apps/, confined and sandboxed
  events.ts    live reload: the repo watched, changes debounced
src/
  data-app.ts  every SDK call: datasets, the reader, mounting a Data App
  App.tsx      layout, routing, live reload
  components/  sidebar, home, stage; ui/ is shadcn/ui
  inspector/   the inspect panel, self-contained to carry to another host; SPEC.md says how
  lib/host.ts  what the page asks the server, apart from the core API

License

Apache-2.0. See LICENSE and NOTICE.

About

No description, website, or topics provided.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Used by

Contributors

Languages