Skip to content

Plugin authoring guide

Use this guide while building and maintaining an OWOX Data Marts plugin. It defines the production plugin contract, SDK usage, UI foundation, deployment, releases, publication, and updates.

If the repository and development tools are not ready yet, first prepare your plugin project.

Create a React and TypeScript application with Vite in the prepared repository:

Terminal window
npm create vite@latest . -- --template react-ts
npm install
npm install @owox/plugin-sdk lucide-react
npm run build

A simple plugin can use this project structure:

.
├── .github/
│ └── workflows/
│ └── deploy-pages.yml
├── src/
│ ├── components/
│ │ └── ui/
│ ├── App.tsx
│ ├── main.tsx
│ └── styles.css
├── .gitignore
├── AGENTS.md
├── index.html
├── package.json
├── plugin.json
├── tsconfig.json
└── vite.config.ts

Keep reusable controls in src/components/ui/. Vite’s default build output is dist/; GitHub Pages publishes that directory. Keep plugin.json at the repository root: OWOX Data Marts reads it from the GitHub Release commit rather than from the deployed page.

An OWOX Data Marts plugin is a small web application hosted outside OWOX Data Marts. OWOX opens the application inside a protected frame and connects it to the installing member through @owox/plugin-sdk.

These constraints follow from the plugin sandbox. No OWOX Data Marts setting relaxes them.

The page runs in an opaque origin. It has no cookies, localStorage, sessionStorage, IndexedDB, or service workers. Anything kept between sessions must live on your own backend, keyed by an identifier received from OWOX Data Marts.

Calls to your backend arrive with Origin: null. The backend must answer Access-Control-Allow-Origin: *, and it cannot authenticate requests with cookies. Pass an identifier explicitly if the backend needs to know who is calling.

Your assets are cross-origin to your page. An opaque origin matches nothing, including the host that served the page, so anything fetched in CORS mode needs Access-Control-Allow-Origin: * from its server. This includes <script type="module">, fonts, and every fetch request. GitHub Pages sends the required header; a plain static server may not. Without it, the page can load while the browser blocks its module script and the plugin runs no code. A page with an inline script and no assets does not encounter this issue.

The entry page must be embeddable. It must not send X-Frame-Options. If it sends a Content-Security-Policy with frame-ancestors, the directive must permit OWOX Data Marts by using *, https:, or the exact origin of the deployment. Publication checks this and refuses a page that could not be displayed.

The plugin never holds an OWOX credential. Calls through ctx.owox are brokered by the host, which attaches the member’s token. Requests have the authority of the member who installed the plugin—never more, and never on behalf of another member. Protected routes still apply their server-side authorization and reject calls that member may not make.

Never place GitHub tokens, OWOX API keys, or other credentials in plugin source files, AGENTS.md, coding-agent prompts, commits, or release notes. Plugin code must not read or store an OWOX credential. Use OWOX_API_KEY only for owox-ctl or an external script running outside the plugin.

  • Add .env and .env.* to .gitignore; keep only a secret-free .env.example when useful.
  • Let gh use its own authenticated credential store.
  • Put backend secrets in the backend host’s secret manager, never in Vite environment variables. Values exposed to browser code are public even when their names contain “secret.”

Import connect from the @owox/plugin-sdk, then use the context it returns:

import { connect } from '@owox/plugin-sdk';
const ctx = await connect();
const dataMarts = await ctx.owox.dataMarts.list();

connect() completes a handshake with the OWOX Data Marts host. It rejects outside an OWOX Data Marts frame or if no host answers within 10 seconds.

Context valueWhat it provides
ctx.owoxAuthenticated OWOX Data Marts API client, supplied by @owox/plugin-sdk. Use the API client documentation to discover available methods. Do not install @owox/api-client or provide an API key inside a plugin; the SDK owns its transport, which cannot be replaced or inspected.
ctx.ui.openExternal(url)Asks the host to open an external HTTPS address in a new tab.
ctx.ui.navigate(path)Asks the host to navigate to a page inside OWOX Data Marts—for example, /ui/${ctx.projectId}/data-marts/${id}—in place of the plugin frame. Resolutions off the app’s origin are refused.
ctx.signalAborts when the host tears the plugin down.
ctx.userId, ctx.projectId, ctx.themeProvides display context without exposing tokens. The member’s name and avatar are available through ctx.owox.auth when needed.

See the API client method reference and Support Matrix for currently supported calls. Requests time out after 30 seconds. Streamed reads do not, because data traversals can run for minutes. At most 32 requests may be in flight at once.

Prefer the typed ctx.owox resources. For an endpoint that has no typed abstraction, the client also exposes getJson<T>(path, query?), postJson<T>(path, body, accept?), putJson<T>(path, body), patchJson<T>(path, body), deleteJson<T = void>(path), and getStream(path, query?). The generic does not validate the response at runtime, so validate returned data yourself. Paths must be root-relative /api/... and are limited to 2,048 characters; unsafe or redirecting paths are refused.

Plugins run in an iframe and do not inherit OWOX Data Marts styles. They must import and bundle their own CSS. @owox/ui is an internal package, not a public plugin dependency, so do not import it.

For now, keep a small UI foundation in the plugin repository. This makes the plugin independent and can later be replaced by an official starter or a versioned plugin UI package without changing the plugin contract.

Use semantic variables and the standard page structure in src/styles.css:

:root {
color-scheme: light;
font-family: Inter, ui-sans-serif, system-ui, sans-serif;
--background: oklch(1 0 0);
--foreground: oklch(0.3346 0.0123 279.25);
--card: oklch(1 0 0);
--muted: oklch(0.97 0 0);
--muted-foreground: oklch(0.5148 0.0128 274.72);
--border: oklch(0.922 0 0);
--primary: oklch(0.6179 0.2295 250.87);
--primary-foreground: oklch(0.985 0 0);
}
:root.dark {
color-scheme: dark;
--background: oklch(0.205 0 0);
--foreground: oklch(0.985 0 0);
--card: oklch(0.145 0 0);
--muted: oklch(0.269 0 0);
--muted-foreground: oklch(0.708 0 0);
--border: oklch(0.269 0 0);
}
* { box-sizing: border-box; }
body { margin: 0; background: var(--background); color: var(--foreground); }
.dm-page { min-height: 100vh; }
.dm-page-header { padding: 1.5rem 3rem; }
.dm-page-header-title { margin: 0; font-size: 1.5rem; font-weight: 500; }
.dm-page-content { padding: 0 3rem 3rem; }
.dm-card { padding: 1rem; border: 1px solid var(--border); border-radius: 0.625rem; background: var(--card); }
.dm-muted { color: var(--muted-foreground); }
@media (max-width: 640px) {
.dm-page-header { padding: 1rem 1.25rem; }
.dm-page-content { padding: 0 1.25rem 1.25rem; }
}

Use those classes for the application shell:

<div className="dm-page">
<header className="dm-page-header">
<h1 className="dm-page-header-title">Plugin name</h1>
</header>
<main className="dm-page-content">
<section className="dm-card">Plugin content</section>
</main>
</div>

After connect(), apply the host theme before rendering:

document.documentElement.classList.toggle('dark', ctx.theme === 'dark');

Keep reusable controls in src/components/ui/. Radix or shadcn-style primitives can be kept locally, and Lucide is a good default for icons. Use semantic variables instead of fixed colors, visible keyboard focus, accessible names, and responsive layouts. Every data-driven screen should include loading, empty, error, and success states.

Do not depend on undocumented host HTML or CSS: it cannot cross the iframe boundary and may change without notice.

Add plugin.json at the repository root:

{
"name": "Example Plugin",
"description": "What this plugin helps a member accomplish",
"delivery": {
"type": "remote",
"url": "https://OWNER.github.io/PLUGIN_NAME/"
}
}

Replace OWNER and PLUGIN_NAME after the first successful deployment. The URL must use HTTPS, be publicly reachable without a sign-in prompt, and not resolve to a private or metadata network, including through redirects.

The manifest contains the plugin-authored fields above. OWOX Data Marts derives plugin identity from the GitHub repository and the current version from the eligible GitHub Release tag. Renaming or transferring a repository does not create another plugin; two repositories with identical contents are two different plugins.

There is no separate API-key or permissions list in plugin.json. The plugin can call only the SDK-supported APIs, acting with the permissions of the member who installed it.

GitHub Pages can host the plugin’s static HTML, CSS, and JavaScript. A repository named OWNER/PLUGIN_NAME normally receives this address:

https://OWNER.github.io/PLUGIN_NAME/

Most plugins use the project-site address above. GitHub Pages serves that site below its repository name, so update vite.config.ts to make scripts, styles, and images use /PLUGIN_NAME/. Vite’s default build output remains dist/.

import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
export default defineConfig({
base: '/PLUGIN_NAME/',
plugins: [react()],
});

Replace PLUGIN_NAME with the exact repository name, then verify that npm run build creates dist/index.html.

Use base: '/' instead when the plugin is served from the domain root: an OWNER.github.io user or organization site, or a custom domain deployed at its root. For example:

export default defineConfig({
base: '/',
plugins: [react()],
});

Create .github/workflows/deploy-pages.yml:

name: Deploy plugin to GitHub Pages
on:
push:
branches: [main]
workflow_dispatch:
permissions:
contents: read
pages: write
id-token: write
concurrency:
group: pages
cancel-in-progress: true
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 22
cache: npm
- run: npm ci
- run: npm run lint --if-present
- run: npm run typecheck --if-present
- run: npm test --if-present
- run: npm run build
- uses: actions/configure-pages@v5
- uses: actions/upload-pages-artifact@v3
with:
path: dist
deploy:
needs: build
runs-on: ubuntu-latest
environment:
name: github-pages
url: ${{ steps.deployment.outputs.page_url }}
steps:
- id: deployment
uses: actions/deploy-pages@v4

The GitHub CLI can ask GitHub to use the Actions workflow:

Terminal window
gh api --method POST repos/OWNER/PLUGIN_NAME/pages -f build_type=workflow

This command needs permission to administer Pages for the repository. If GitHub refuses it, open Settings → Pages in the repository and select GitHub Actions under Build and deployment → Source. GitHub documents both the Pages API and the web settings.

After pushing, find and watch the workflow:

Terminal window
gh run list --workflow deploy-pages.yml --limit 1
gh run watch RUN_ID --exit-status

Replace RUN_ID with the value shown by the first command. Open the deployed address and confirm that it loads over HTTPS without a sign-in prompt. Put that exact address in plugin.json, commit and push the manifest change, and wait for the deployment again before creating a release.

OWOX Data Marts reads versions from GitHub Releases. A release becomes eligible when it is:

  • published rather than saved as a draft;
  • not marked as a GitHub prerelease;
  • tagged exactly MAJOR.MINOR.PATCH, optionally with a leading v;
  • attached to a resolvable commit with a valid root plugin.json;
  • configured with a public, embeddable delivery page.

After the deployment succeeds, create the first production release:

Terminal window
gh release create v0.1.0 --target main --generate-notes

To use GitHub’s web interface instead, open Releases, select Draft a new release, create the tag v0.1.0 from main, describe the change, leave Set as a pre-release unchecked, and select Publish release. See Managing releases.

Prerelease identifiers such as v1.2.3-rc.1 and build metadata such as v1.2.3+build.7 are not eligible, even though both are valid SemVer. Use GitHub’s prerelease checkbox for test releases.

The highest eligible version becomes current for everyone using the plugin, and members cannot pin an older version.

Publishing makes a plugin findable but installs it for nobody. Every member decides whether to install it, and unpublishing later does not uninstall existing installations.

  1. Open Plugins in OWOX Data Marts.
  2. Select Publish Plugin.
  3. Enter OWNER/PLUGIN_NAME in GitHub repository.
  4. Under Who can find it, choose Only me for testing. A Project Admin can choose Everyone in this project after the plugin is ready.
  5. Publish the plugin, find it in the Gallery, select it, and confirm installation.

For an agent or script, use the already configured owox-ctl:

Terminal window
# Publish only for yourself while testing.
owox-ctl plugins publish OWNER/PLUGIN_NAME --scope member
# Publish for everyone in the project. Requires Project Admin access.
owox-ctl plugins publish OWNER/PLUGIN_NAME --scope project
# Deployment administrators can publish to selected projects.
owox-ctl plugins publish OWNER/PLUGIN_NAME --scope deployment --project-id PROJECT_ID
# Deployment administrators can publish to every current and future project.
owox-ctl plugins publish OWNER/PLUGIN_NAME --scope deployment --all-projects

Member publication is the recommended first step for authors. Deployment scope is restricted to deployment administrators using API keys named in OWOX_DEPLOYMENT_PLUGIN_PUBLISHER_API_KEY_IDS; it also requires either one or more --project-id choices or --all-projects. Use selected project IDs when possible: --all-projects makes the plugin findable in every current and future project, and projects cannot be excluded from that audience.

A public repository needs no GitHub App setup. For a private repository, publish once and follow the installation link returned by OWOX Data Marts to grant the correct GitHub App access, then publish again. A self-managed deployment may use its own GitHub App, so use the link returned by that deployment.

Publish the first release to Only me, install it, and test it inside OWOX Data Marts. The frontend depends on the host iframe and the real @owox/plugin-sdk handshake, so a standalone local frontend is not a supported preview environment.

Exercise the key flows with the installing member’s real access. Confirm loading, empty, error, and success states; light and dark themes; narrow and wide layouts; navigation; and every ctx.owox call the plugin uses. Fix issues, deploy the corrected files, and create a higher eligible release before sharing the plugin more broadly.

For a normal update, change and test the plugin, deploy it, and create a higher production release such as v0.1.1. OWOX Data Marts checks relevant repositories daily. Ask for an immediate check when needed:

Terminal window
owox-ctl plugins update OWNER/PLUGIN_NAME

A valid higher release applies to every installation. There is no member-level version pinning. To roll back, restore the last working source, deploy it, and create a new higher patch release that explains the restoration; do not try to move or recreate an older tag.

OWOX Data Marts records the exact commit referenced by each eligible release, but the delivery URL does not pin the files served there. Once an eligible release version is recorded, moving, deleting, or recreating its tag cannot rewrite that recorded version; use a new higher eligible release to roll back. Deploying different files changes what installed members run even before another release is created. Treat every deployment to the production URL as a production change and keep its release metadata aligned.

If the plugin cannot be published or opens as a blank page, verify that:

  • npm run build succeeds and creates dist/index.html;
  • the GitHub Actions workflow uploads dist;
  • Vite’s base exactly matches the deployed path: /PLUGIN_NAME/ for a project site, or / for a user or organization site or custom-domain root deployment;
  • the Pages deployment succeeds and the HTTPS address opens without signing in;
  • plugin.json is at the repository root and contains that exact delivery address;
  • the GitHub Release is published, is not a prerelease, and has an eligible version tag;
  • scripts, fonts, and fetched assets are served with Access-Control-Allow-Origin: *;
  • the entry page is not blocked by X-Frame-Options or frame-ancestors;
  • the OWOX Data Marts GitHub App can read the repository when it is private;
  • the plugin catches errors and shows a useful message instead of a blank screen.

An administrator can suspend a plugin across a deployment. A suspended plugin cannot be opened, installed, or restored, but existing installations are not removed. Resuming makes it available again on the current version.

Before sharing the plugin with a project, confirm that:

  • the repository contains AGENTS.md, source files, plugin.json, and the Pages workflow;
  • build, lint, type checking, and tests pass;
  • the installed personal test in OWOX Data Marts covers narrow and wide layouts, light and dark themes, and understandable loading, empty, error, and success states;
  • no credential or .env file is committed;
  • the deployed page and all its assets load from a public HTTPS address;
  • the production release points to the intended commit and has an eligible version tag;
  • the plugin has been published to Only me, installed, and tested with the installing member’s real access, including every ctx.owox call it uses;
  • the plugin description matches what the plugin actually does;
  • project-wide publication happens only after the personal installation passes.