npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@colixsystems/widget-sdk

v0.18.0

Published

Common widget interface for AppStudio. Implements WidgetManifest, WidgetContext, property schema, and helper hooks.

Readme

@colixsystems/widget-sdk

Common widget interface for AppStudio. This package implements the contract that every widget — built-in or third-party, web or native — speaks: a WidgetManifest, a WidgetContext, a property schema, the helper hooks, and the static linter that gates submissions.

See the design reference for the full architecture: docs/architecture/widget-marketplace.md, specifically section 3.1.

Status

v0.18.0 — pre-publish. The package surface (types, function names, export paths) is the v1 contract; runtime behaviour for some hooks is stubbed (each hook documents what's wired and what isn't). It is not yet published to npm.

What's new in 0.18.0

REQ-ACL-06 / REQ-ACL-RELINHERIT-05 — per-record VirtualPermission management from inside a widget.

  • useRecordPermissions(tableId, recordId) is wired + the new WidgetContext.recordPermissions slice + the new acl.write:records scope. Returns { permissions, loading, error, grant, revoke, update, refetch } where permissions is Array<{ id, principalType: "USER" | "GROUP" | "PUBLIC", principalId, canRead, canWrite, canDelete, canGrant }>. Hits the existing REQ-ACL-06 /api/v1/tables/:tableId/records/:recordId/permissions REST surface; the host normalises the wire shape (userId / groupId) into the principalType + principalId pair the hook returns. Rejections from grant / revoke / update surface as a new structured PermissionError (named export) with codeFORBIDDEN | VALIDATION | NOT_FOUND | CONFLICT | INTERNAL. When tableId or recordId is null/empty the hook collapses to a stable empty no-op result without a network round-trip. Mutating requires acl.write:records AND canGrant on the target record — Studio owners short-circuit, APP_USER actors hold canGrant via REQ-ACL-05 (creator-grant) or a delegated REQ-ACL-06 grant. Backs the rewritten built-in Chat widget's "+ New channel" + DM-create flows, where the channel creator grants canRead / canWrite / canGrant to each invitee without going through Studio. Additive.
  • CONTRACT.version1.8.0 (additive: one new hook, one new context slice, one new scope, one new error class). No existing export changed signature.

What was in 0.17.0

REQ-WBLT-FORMBUILDER — a runtime schema resolver so widgets can render by column type.

  • useDatastoreSchema(tableId) is wired + the new WidgetContext.datastore.schema slice. Returns { schema, loading, error, refetch } where schema is { id, name, columns: [{ id, name, dataType, required, relationType, targetTableId, isIdentification }] } (null until loaded). It reads the existing ACL-gated GET /api/v1/tables/:id — structure only, never row data — so a public-grant table resolves for anonymous visitors exactly like a record read. Use it to resolve a stored columnId to its column name / dataType / relation target at runtime (the built-in Form Builder uses it to render an input per column type). Reads need the datastore.read:<table> scope. Backed by a new datastore.schema(tableId) host facade (web: widgetHostDatastore; native: hostDatastore in the export's widgetHost.js). Additive.
  • CONTRACT.version1.7.0 (additive: one new hook, one new datastore.schema context field). No existing export changed signature.

What's new in 0.16.0

REQ-THEME — the tenant's Theme Settings now flow all the way into useTheme().

  • themeTokens.colors gains secondary + onSecondary. useTheme().colors.secondary reflects the tenant's Secondary Color picker (with onSecondary as its readable contrast color), alongside the existing primary / onPrimary. Built-in widgets like Button use it for their secondary variant; third-party widgets can use it for a branded second accent. The full colors shape is now { primary, onPrimary, secondary, onSecondary, surface, onSurface, surfaceMuted, onSurfaceMuted, border, danger, success, warning, info }.
  • colors.primary / colors.secondary / typography.fontFamily are tenant-resolved. The host maps the Studio Theme Settings blob (Primary Color, Secondary Color, Global Font) onto the default tokens before handing them to useTheme(), on both the live Player and the exported app — so a widget that reads tokens re-themes automatically. (Custom Google fonts render in the Player today; the exported app falls back to the system face for non-system fonts until font bundling lands.)
  • CONTRACT.version1.6.0 (additive: two new themeTokens.colors keys). No existing export changed signature.

What's new in 0.15.0

REQ-WSDK-PLATFORM — the "split-implementation + vetted package list" pivot. See docs/design/req-widget-sdk-cross-platform-primitives.md for the full design and rationale.

  • CONTRACT.vettedImports (new). A curated allowlist of bare specifiers a widget may import — react, @colixsystems/widget-sdk, react-native, axios, date-fns, react-native-svg, lucide-react-native, react-native-maps, leaflet, react-leaflet, expo-av, @react-native-community/datetimepicker, expo-clipboard, expo-haptics. Each entry carries platforms (one or both of "web" / "native") and a category so the linter and the marketplace listing can render honest platform badges. CONTRACT.allowedBareImports (the existing field) is now derived from vettedImports and stays a plain string[] for back-compat.
  • fetch and XMLHttpRequest come off CONTRACT.bannedApis. Widgets may call third-party APIs directly. Calls to the host's own /api/* surface will 401 because the JWT token is never shared with widget code; the linter emits a soft no-host-api-url warning when it sees host-URL substrings so authors learn the rule statically. Use SDK hooks (useDatastoreQuery, useUsers, useFile, …) for workspace data; use axios / fetch for third-party APIs.
  • import-not-vetted linter rule (new). Every bare import specifier is validated against CONTRACT.vettedImports. Relative imports inside the bundle (./shared.js) are allowed so split-impl widgets can share helpers; ../ and absolute paths are rejected.
  • import-platform-mismatch linter rule (new). A single-source widget that imports a native-only package while manifest.supportedPlatforms includes "web" fails the lint. The author either drops the platform from the manifest OR ships a widget.web.jsx + widget.native.jsx pair where the platform-specific import lives in the file that targets its platform.
  • Lint findings carry severity. "error" (default) blocks publish; "warning" (currently only no-host-api-url) surfaces to reviewers without blocking. The lintSource(...) return shape stays { ok, findings }ok is true iff no error-severity findings exist.
  • Four Tier A SDK additions:
    • <Icon> primitive — <Icon name="check" size={16} color={theme.colors.primary} />. Wraps lucide-react-native; works on both platforms.
    • <DateTimePicker> primitive — <DateTimePicker value={iso} onChange={iso => …} mode="date" | "time" | "datetime" />. Wraps @react-native-community/datetimepicker and normalizes the value to ISO 8601 strings (the datastore wire format).
    • useClipboard() hook — { copy, paste, hasContent }. Web via navigator.clipboard; native via expo-clipboard. Rejections are a structured ClipboardError with .code in PERMISSION_DENIED | INTERNAL.
    • useToast() hook — { showToast }. The host installs a workspace-themed renderer at WidgetContext.toast.showToast; if omitted, the web variant dispatches an appstudio:widget-toast CustomEvent and native logs to the console.
  • CONTRACT.version1.5.0 (additive: two new contract fields — vettedImports, hostApiUrlPatterns — two banned APIs removed, two primitives + two hooks added, one optional widgetContextShape.toast slot). No existing export changed signature.

What was in 0.14.1

  • groupRef property type (REQ-USERMGMT M4 / §4.8). Authors can now declare { type: 'groupRef', label: 'Group' } in their propertySchema to render a Group picker in the Studio Properties Panel. The widget receives a bare AppUserGroup UUID — REQ-GEN-07 compliant, so tenant-copy walks the value transparently. Used by the built-in appstudio.user-management widget for its defaultGroupId prop and available to third-party widgets that need to anchor behaviour on a specific group.
  • Patch bump — additive enumeration entry, no exported function signature changed. CONTRACT.version stays 1.4.0.

What's new in 0.14.0

  • useUsers() + useGroups() — AppUser administration hooks (REQ-USERMGMT / REQ-ACL-SYS M3). A widget can now invite, deactivate, reactivate, and remove members, and create / delete groups + add / remove members, from a published-app surface. Returns { users | groups, loading, error, refetch, ... } plus imperative mutation methods. Reads gated by users.read:* / groups.read:*; mutations by users.write:* / groups.write:*. Rejections surface as a structured DirectoryError (new named export) with codeFORBIDDEN | VALIDATION | NOT_FOUND | INVITE_ONLY. The host signs an X-Widget-Scopes header against JWT_SECRET so an APP_USER cannot forge a scope set, and the backend additionally gates the request behind a SystemAcl users.* / groups.* capability grant (REQ-ACL-SYS M1 + M3).
  • Managing app users from a widget — see the section below.
  • New linter rule no-scope-mismatch-useUsers / no-scope-mismatch-useGroups. Calling useUsers().invite() / .deactivate() / .reactivate() / .remove() without users.write:* in the manifest's requestedScopes fails the lint; calling useGroups() mutation methods without groups.write:* fails the lint. Keeps the manifest honest at submission time.
  • CONTRACT.version1.4.0 (additive: two new hooks, two new context slices, six new scope verbs, one new error class). No existing export changed signature.

What's new in 0.13.0

  • WidgetTree component + useChildRenderer() hook wired. Container widgets (Tabs, Card, …) can now render arbitrary author-authored child page-tree nodes through the SDK without importing the host renderer. The host pre-binds the surrounding render context (breakpoint, page ctx, parent) into a closure on ctx.renderer.renderNode(node) — the widget just passes the child node. Additive.

What's new in 0.12.0

  • useDatastoreRecord(tableId, recordId) is wired. Returns { data, loading, error, refetch } for a single record fetched through the host's records(table).get(id). Sister to useDatastoreQuery; mirrors its ref discipline so refetch stays a stable callback identity. A 404 surfaces as DatastoreError.code === "NOT_FOUND". Additive.
  • useFile(fileId) is wired + new WidgetContext.files slice. Returns { url, file, loading, error, refetch } — the url is an absolute URL the widget can drop straight into <Image source>. Backed by a new files.get(fileId) host facade (web: widgetHostFiles through api/client; native: hostFiles in the export's widgetHost.js). Additive.

What's new in 0.11.0

  • useNavigation() is wired. Returns the host-provided navigation surface { goTo, goBack, push, replace, back, currentRoute } for internal page-to-page navigation. Missing methods degrade to no-ops on the Studio canvas preview. Additive.
  • Linking primitive re-exported. Linking.openURL(url) opens an external URL with the OS handler — web (react-native-web) maps to window.open / location.href; native hands off to the system. Use this for external URLs; use useNavigation().goTo(pageId) for internal pages.

What's new in 0.10.0

  • useUser() is wired. Returns the active end-user identity { id, email, displayName, roles, groupIds } from the host-provided WidgetContext. id is null for anonymous visitors and on the Studio canvas preview. All fields are guaranteed present (the host fills safe defaults), so widgets read them without optional chaining. Additive — no migration needed for existing widgets.

What's new in 0.9.0

  • usePayments() — incoming app-user payments (REQ-BILL-07-WIDGETPAY). Returns { requestPayment, getPayment }. requestPayment({ amountCents, currency?, description, metadata?, returnPath? }) triggers a one-time charge from the signed-in app user and resolves to { id, status, checkoutUrl? }: when checkoutUrl is present the widget opens it (web: navigate; native: expo-web-browser) — the user pays in hosted Stripe Checkout; when absent (the platform's built-in mock provider, the default until Stripe is configured) the charge auto-confirms (status: "PAID"). getPayment(id) polls the terminal status. Backed by a new WidgetContext.payments slice and gated by the new payments.charge:appUser scope. No card data ever touches the widget — never collect card fields yourself. The charge settles to the workspace owner; the amount is bounded by a platform per-charge cap. Rejections are a structured PaymentError (also a new named export) with a stable .code.
  • CONTRACT.version1.2.0 (additive: one new hook, one new context slice, one new scope, one new error class). No existing export changed signature.

What was in 0.8.0

  • useDirectory() — read-only user directory hook (REQ-DIR-01). Returns { users, loading, error, refetch } where each user is { id, name, role }. Backed by a new WidgetContext.directory.listUsers(query) slice and gated by the new directory.read:users scope. Use it to build a chat people-list, an @-mention picker, or to resolve an author id to a display name. The host reads GET /api/v1/app/users, which hands non-Studio (Player) callers the reduced { id, name, role } projection — email and other admin-only fields never leave the server for an app end-user. query is an optional { q, role, isActive, limit, offset } (q substring-matches the display name; role is "USER" (default), "INTEGRATION", or "ALL"). Mutating users is not part of the widget surface — the directory is read-only.
  • CONTRACT.version1.1.0 (additive: one new hook, one new context slice, one new scope). No existing export changed signature.

What was in 0.7.0

  • datastoreTemplate is now part of the public manifest contract. CONTRACT.manifestSchema carries an optional datastoreTemplate entry alongside the existing fields. The TypeScript WidgetManifest already declared this since 0.3.0, but the runtime contract did not — the agent's system prompt only generated the field set advertised by CONTRACT.manifestSchema, which silently omitted datastoreTemplate from every AI-generated draft. The mismatch meant most AI-generated DATA widgets shipped without a seeded table, forcing the end-user to hand-build it before the widget would render anything useful. With the schema entry in place, the agent now defaults to including a datastoreTemplate whenever the widget reads or writes data, matching the no-code experience the platform promises.
  • Agent system prompt (backend/src/core/services/ai-widget-agent.service.js) gains a dedicated ===== DATASTORE TEMPLATE ===== section, an updated DATA-widget example with a template, and a CONVERSATION BEHAVIOUR rule pinning the default. The note-list example (TextInput + create + update + delete) now shows the matching template too, including the column-name-match rule (record.Body"name": "Body").

What was in 0.6.0

  • Primitives are now React Native through-and-through. Web (Studio + Player) bundles with react-native-web; native (exported Expo app) uses the real react-native. The hand-written DOM wrappers in primitives.js are gone — both web and native files are now one-line re-exports from react-native. The widget API surface is unchanged for the components that already existed (Text, View, Pressable, Image, ScrollView, TextInput), so existing 0.5.0 widgets keep running without source edits.
  • More primitives available for free. FlatList, SectionList, ActivityIndicator, Switch, and StyleSheet are now exported alongside the original five. Authors who want them just import from @colixsystems/widget-sdk like any other primitive.
  • Per-component API docs link out to React Native. The Developer Widgets page and the AI agent's system prompt now point at https://reactnative.dev/docs/ for prop details instead of duplicating them in the SDK contract. Less drift, more breadth (every RN prop — accessibilityLabel, testID, gesture handlers, …, works without explicit allowlisting).
  • Adding a new primitive is now a single edit. Append the export name to primitives.js + primitives.native.js + the CONTRACT.primitives array. No web/native implementation split.
  • New peer dep. react-native-web >=0.19.0 (optional, marked under peerDependenciesMeta). The web bundler picks it up via an alias (react-nativereact-native-web in the host's Vite / webpack config); the native bundler ignores it.

What was in 0.5.0

  • TextInput primitive. Cross-platform controlled text field — web maps to <input type="text"> (or <textarea> when multiline is true), native maps to react-native TextInput. Props: value, onChangeText, placeholder, multiline, rows, disabled, style. Fixes the gap that forced widgets to fall back to raw <textarea> (web-only). The AI Widget Agent's system prompt now documents this primitive.
  • useDatastoreMutation(...).update(id, partial) is wired. Hits PATCH /api/v1/tables/:tableId/records/:id, which now exists on the backend (REQ-DML-PATCH). Partial-update semantics — only the supplied columns are mutated, the rest of the row is left intact. MANY_TO_MANY relations follow replace-set semantics when supplied; omitting the column leaves the existing links alone. Constraint enforcement (REQ-CONS-04) runs against the merged row, excluding self so a re-affirm doesn't trip its own UNIQUE.

What was in 0.4.1

  • useDatastoreQuery returns a stable refetch identity. The hook no longer rebinds the underlying callback when the host's WidgetContext value (a fresh object identity on every render in Studio + PageRenderer) changes. Widgets that put refetch in a useEffect dep array no longer loop.

What's new in 0.4.0

  • CONTRACT is now a named export. A frozen object literal describing the SDK surface every consumer (LLM system prompt, static analyzer, host builder, contract tests) derives from instead of declaring its own copy. See docs/design/ai-widget-contract.md for the full design and src/contract.js for the source. The contract carries:

    • hooks — name, signature, return shape, required WidgetContext slices, manifest scopes.
    • primitives — the cross-platform primitive list (names + the React Native component each one backs).
    • manifestSchema — the authoritative WidgetManifest field list (with id and minAppStudioVersion, not the legacy manifestId / minAppStudioVer).
    • themeTokens — the default useTheme() payload.
    • widgetContextShape — what the host must populate.
    • bundleExportContract — the two default-export shapes the loader accepts.
    • bannedApis — the global allowlist gate.
    • allowedBareImportsreact, @colixsystems/widget-sdk.
  • useDatastoreQuery is now stateful. Returns { data, loading, error, refetch }. Previously the hook returned the raw list(...) promise; widgets that called .map(...) synchronously on the result threw on first render. Migration: replace const rows = useDatastoreQuery(...) with const { data, loading, error } = useDatastoreQuery(...). data is always an array (empty when the table is unbound or loading).

  • DatastoreError is now a named export. Mutations throw a structured DatastoreError with .code (VALIDATION / CONSTRAINT_VIOLATION / FORBIDDEN / NOT_FOUND / INTERNAL) and an optional .fieldErrors map populated from the host's 422 payload. Widgets can branch on err.code without parsing axios messages.

  • useI18n().t now honours fallback. t(key, fallback) returns fallback when the host's translation table has no entry for key.

What was in 0.3.0

Additive: WidgetManifest carries an optional datastoreTemplate field. When a tenant installs a widget that declares one, the table set is seeded into their workspace alongside the install. Tables follow the existing built-in template semantics (auto-suffixed naming, REQ-ACL-05 creator-grants, REQ-TEMPLATES-ACL public grants, REQ-ACL-RELINHERIT cross-relation inheritance) and persist when the widget is later uninstalled. See WidgetDatastoreTemplate in src/index.d.ts for the structural constraints — at most 8 tables per widget, 24 columns per table, RELATION columns address siblings by suffix.

Public API

import { defineWidget, validateManifest, useDatastoreQuery, Text, View } from "@colixsystems/widget-sdk";
  • defineWidget({ manifest, component }) — validates the manifest and produces a widget module the host can register.
  • validateManifest(m) / validatePropertySchema(s) / validateProps(schema, props) — shape validation; no third-party deps.
  • useDatastoreQuery, useDatastoreRecord, useDatastoreSchema, useDatastoreMutation, useDirectory, useUsers, useGroups, useRecordPermissions, useFile, useWidgetEvent, usePayments, useTheme, useI18n, useUser, useNavigation, useChildRenderer, useClipboard, useToast — hooks that read from the host-provided WidgetContext (or, for useClipboard, the platform clipboard API directly). useDirectory(query?) returns { users, loading, error, refetch } (each user { id, name, role }) and requires the directory.read:users scope. useUsers(query?) returns { users, loading, error, refetch, invite, deactivate, reactivate, remove } and requires users.read:* (mutations also need users.write:*); rejections are a DirectoryError. useGroups(query?) returns { groups, loading, error, refetch, create, remove, addMember, removeMember } and requires groups.read:* (mutations also need groups.write:*). usePayments() returns { requestPayment, getPayment } and requires the payments.charge:appUser scope; requestPayment(...) rejects with a PaymentError. useUser() returns the active end-user identity { id, email, displayName, roles, groupIds } (id is null for anonymous / preview). useNavigation() returns { goTo, goBack, push, replace, back, currentRoute } for internal page navigation — for external URLs use the Linking primitive (Linking.openURL(url)). useDatastoreRecord(tableId, recordId) returns { data, loading, error, refetch } for a single record (data is one row or null). useDatastoreSchema(tableId) returns { schema, loading, error, refetch } where schema is { id, name, columns: [{ id, name, dataType, required, relationType, targetTableId, isIdentification }] } (structure only, no row data) — use it to resolve a stored columnId to its column type at runtime; requires the datastore.read:<table> scope. useFile(fileId) returns { url, file, loading, error, refetch } — the url is an absolute URL composed against the host's API base. useChildRenderer() returns { renderNode(node) } — container widgets call it to render arbitrary child page-tree nodes (prefer the WidgetTree component for the common case).
  • WidgetTree({ node }) — component that renders an author-authored child node through the host's renderer; used by Tabs / Card / custom containers to host arbitrary child widgets.
  • Text, View, Pressable, Image, ScrollView, TextInput, FlatList, SectionList, ActivityIndicator, Switch, StyleSheet, Linking, Icon, DateTimePicker — re-exported from react-native (the RN primitives) or implemented in the SDK (Icon wraps lucide-react-native, DateTimePicker wraps @react-native-community/datetimepicker). The web build aliases react-native to react-native-web so widgets render in the browser without any per-platform code; the exported Expo app's Metro bundler resolves the real react-native library. Linking is a static API (Linking.openURL(url)) — use it for external URLs, and use useNavigation().goTo(pageId) for internal page navigation. See https://reactnative.dev/docs/ for per-component props.
  • WidgetContextProvider — React context provider that the host (Studio, Player, exported app) wraps widgets with.

Managing app users from a widget

REQ-USERMGMT / REQ-ACL-SYS M3 added two hooks that let a widget invite, deactivate, reactivate, and remove members, plus create / delete groups and add / remove their members. The hooks are gated by two layers: the widget's manifest must declare the scope (so the static analyzer + the host's signed X-Widget-Scopes header agree), and the calling APP_USER must hold the matching users.* / groups.* capability in the tenant (a SystemAcl grant the Studio admin issues via the Roles UI). A widget that declares users.write:* but whose caller does not hold the grant gets a DirectoryError with code: 'FORBIDDEN' — surface that to the end-user as a "you do not have permission to do that" message.

import { Text, View, Pressable, useUsers, useGroups } from "@colixsystems/widget-sdk";

export default function MemberManager() {
  const { users, loading, invite, deactivate, remove } = useUsers({ q: "" });
  const { groups, addMember } = useGroups();
  if (loading) return <Text>Loading…</Text>;
  return (
    <View>
      {users.map((u) => (
        <View key={u.id}>
          <Text>{u.name} — {u.isActive ? "active" : "inactive"}</Text>
          <Pressable onPress={() => deactivate(u.id)}><Text>Deactivate</Text></Pressable>
          <Pressable onPress={() => remove(u.id)}><Text>Remove</Text></Pressable>
        </View>
      ))}
      <Pressable
        onPress={async () => {
          try {
            await invite({ email: "[email protected]", name: "New User", groupIds: [groups[0]?.id].filter(Boolean) });
          } catch (err) {
            // err.code is one of FORBIDDEN | VALIDATION | NOT_FOUND | INVITE_ONLY
          }
        }}
      >
        <Text>Invite</Text>
      </Pressable>
    </View>
  );
}

The matching manifest declares the scopes:

{
  // ...
  requestedScopes: ["users.read:*", "users.write:*", "groups.read:*", "groups.write:*"],
}

The host rejects calls whose scope is not declared in the manifest (the SDK linter catches this statically too). Declaring a write scope is also a consent prompt the Studio admin sees at install time — the wider the scope set, the more careful the admin is about granting the install.

Managing per-record permissions from a widget

REQ-ACL-06 / REQ-ACL-RELINHERIT-05 added useRecordPermissions(tableId, recordId), the in-app surface for sharing a single record with another user or group. The chat widget uses it to invite members into a channel — the channel record's per-record grants ARE the membership list (Messages inherit those grants via REQ-ACL-RELINHERIT). The hook also covers project-workspace, document-sharing, and team-roster widgets that grant access on a record-by-record basis.

import { Text, View, Pressable, useRecordPermissions, PermissionError } from "@colixsystems/widget-sdk";

export default function ShareRecord({ tableId, recordId, partnerUserId }) {
  const { permissions, loading, grant, revoke } = useRecordPermissions(tableId, recordId);
  if (loading) return <Text>Loading members…</Text>;
  return (
    <View>
      {permissions.map((p) => (
        <View key={p.id}>
          <Text>{p.principalType}:{p.principalId} — {p.canWrite ? "writer" : "reader"}</Text>
          <Pressable onPress={() => revoke(p.id)}><Text>Remove</Text></Pressable>
        </View>
      ))}
      <Pressable
        onPress={async () => {
          try {
            await grant({
              principalType: "USER",
              principalId: partnerUserId,
              canRead: true,
              canWrite: true,
              canGrant: true,
            });
          } catch (err) {
            if (err instanceof PermissionError && err.code === "FORBIDDEN") {
              // The signed-in user lacks canGrant on this record.
            }
          }
        }}
      >
        <Text>Invite partner</Text>
      </Pressable>
    </View>
  );
}

The manifest declares the matching scope:

{
  // ...
  requestedScopes: ["acl.write:records"],
}

The server-side gate is canGrant on the target record — Studio owners short-circuit, and APP_USER actors who hold canGrant (via REQ-ACL-05 creator-grant, or a delegated REQ-ACL-06 grant) pass. A caller without canGrant receives PermissionError { code: "FORBIDDEN" }. The hook collapses to a stable no-op when tableId or recordId is null/empty — so a widget can render its picker first, then bind to the picked record without any conditional hook tricks.

Linter

npx appstudio-widget lint path/to/widget.js

Scans for banned patterns (eval, new Function, dynamic import(), direct imports of host stores, raw axios). Exits 1 on findings.

import { lintSource } from "@colixsystems/widget-sdk/linter";
const report = lintSource(source);

Why no TypeScript dependency?

The package is plain ESM JavaScript with hand-written .d.ts ambient types. Consumers using TypeScript get full IntelliSense; consumers using JavaScript pay nothing.