maypop-sdk
The framework-neutral browser SDK for apps running on Maypop. It exposes the
same object and capabilities as the hosted /sdk/v1.js script: identity, KV,
Drive, AI, integrations, multiplayer, notifications, and the other APIs in the
typed Maypop contract.
Install
pnpm add @basilica-digital/maypop-sdk
Import the SDK from browser code and wait for the Maypop host before using a capability:
import { maypop } from "@basilica-digital/maypop-sdk";
await maypop.ready();
const todos = await maypop.kv.list({ prefix: "todo/" });
The default and named exports are the exact same object as window.maypop, so
framework code and scripts can interoperate without adapters:
import maypop, { type Maypop } from "@basilica-digital/maypop-sdk";
function currentUser(sdk: Maypop = maypop) {
return sdk.user;
}
The module is safe to import while a framework renders on the server. Accessing
the SDK there still throws because capabilities belong to the browser-hosted app
session. Call getMaypop() from browser-only code when an SSR framework needs
to defer access explicitly.
React
React apps can subscribe to Maypop state without manually coordinating the host handshake or effect cleanup:
import {
useMaypopKV,
useMaypopMode,
useMaypopSession,
} from "@basilica-digital/maypop-sdk/react";
export function Counter() {
const session = useMaypopSession();
const mode = useMaypopMode();
const count = useMaypopKV("counter", 0);
if (session.status === "connecting") return <p>Connecting…</p>;
if (session.status === "error") return <p>{session.error.message}</p>;
return (
<button
disabled={mode === "read-only" || count.isMutating}
onClick={() => count.setValue(count.value + 1)}
>
{count.value}
</button>
);
}
The hooks use the hosted SDK automatically. MaypopProvider is only needed to
inject another client in a test or future local sandbox host:
import { MaypopProvider } from "@basilica-digital/maypop-sdk/react";
<MaypopProvider client={sandboxClient}>
<App />
</MaypopProvider>
Local development hosts
Vite
Add the Maypop plugin to an existing Vite configuration:
import { maypop } from "@basilica-digital/maypop-sdk/vite";
import { defineConfig } from "vite";
export default defineConfig({
plugins: [maypop()],
});
Rsbuild
Add the equivalent plugin to an Rsbuild configuration:
import { defineConfig } from "@rsbuild/core";
import { maypop } from "@basilica-digital/maypop-sdk/rsbuild";
export default defineConfig({
plugins: [maypop()],
});
Next.js
Wrap a Next.js configuration. The wrapper adds its sandbox proxy only to
next dev; builds and production servers keep the original configuration:
import { withMaypop } from "@basilica-digital/maypop-sdk/next";
export default withMaypop({
output: "export",
});
The framework's normal development command then serves its URL as a Maypop host
with the application inside it, including when a routed application opens or
refreshes a deep URL. The app uses the production SDK handshake, iframe
permissions, and API shapes. Shared KV state persists to
.maypop/local/kv.json, while Drive metadata and bytes persist under
.maypop/local/drive/.
The Next.js binding also adds the machine's active network addresses to
allowedDevOrigins, so opening the development server from another device on
the LAN loads and hydrates its client scripts. Any configured entries are
preserved.
Identity, members, KV, Drive, mock MCP integrations, share-card previews, and a
notification outbox are available locally. Their capability scopes are
reported through maypop.user.scopes; APIs that need another capability fail
with maypop/unsupported when they reach the local host. maypop.apps.open()
and group-settings navigation remain unavailable because they require the
Maypop product shell rather than an app API.
maypop.notify() is captured by default rather than delivered. Open the
Maypop badge in the development host, or visit /_maypop/notifications, to
inspect the requested and resolved recipients, title, body, and deep link.
Captured calls persist in .maypop/local/notifications.json.
Authenticated development
Create .maypop/local/dev.json to opt this machine into authenticated
capabilities. Git ignores it with the rest of .maypop/local/: it selects a
developer account and can enable access to real app data or billable AI calls.
Hybrid mode keeps KV and Drive local while forwarding an explicit set of
read-only or non-data capabilities through the account authenticated by
maypop auth:
{
"mode": "hybrid",
"profile": "dev",
"remoteCapabilities": ["ai", "members", "link", "mcp", "multiplayer"],
"notifications": "inspect"
}
ai includes chat completions, streaming, decisions (typed classification,
routing, gating, and scoring on TypeSafe's Jev model), images, video, audio,
transcription, and maypop.agent (the development host serves its lazy browser
chunk). These calls use the selected account's real allowance and can consume
credits. members reads the real app roster, link enables server-side URL
unfurling, mcp exposes the app's real linked MCP servers and tools, and
multiplayer uses the real rendezvous API while loading the Iroh JS/Wasm
runtime locally. KV and Drive remain local, and notifications remain in the
inspector. Connect a server with maypop mcp connect, then run maypop mcp link in the app repository before starting the development server. A hybrid
configuration cannot combine the real mcp capability with
.maypop/local/mcp.json fixtures.
Connected mode skips local KV/Drive initialization and sends the entire app API surface to the real app:
{
"mode": "connected",
"profile": "dev",
"notifications": "inspect"
}
The project must already be connected by maypop init, and the selected CLI
profile must be authenticated and have access to that app. Omit profile to
use the normal CLI selection rules, including MAYPOP_PROFILE and the profile
whose API URL matches the repository. The Node development host reads the
owner-only profile store written by maypop auth and mints a short-lived,
app-scoped session itself. The saved CLI credential and session refresh token
are never exposed to application code or written under .maypop/.
Real notification delivery requires connected mode and a second explicit opt-in:
{
"mode": "connected",
"notifications": "live"
}
Use "notifications": "disabled" to remove notification permission entirely.
The default is "inspect" in every development mode.
Local identities and audiences
Sandbox mode can start as an anonymous, reader, writer, editor, or admin viewer and expose a configurable audience. This exercises the same public identity, mode, scope, members, and sign-in behavior as the hosted SDK:
{
"mode": "sandbox",
"viewer": {
"username": "Visitor",
"role": "reader",
"anonymous": true,
"signInGrantsWrite": true,
"signedInUsername": "Alice",
"signedInRole": "writer"
},
"members": [
{ "username": "Bob", "role": "editor", "connected": false }
],
"guestCount": 2
}
Calling maypop.signIn() in this fixture upgrades the local session and
reloads the app as the configured signed-in viewer. The local API enforces
read-only KV, Drive, and notification scopes, so direct requests cannot bypass
the browser SDK's permission checks.
Local KV writes validate the production key and value limits. Set
"strictStorage": true to additionally enforce a committed
.maypop/kv-policy.json during local pulls and pushes. Preset and advanced
policies use the same roles, key patterns, and ownership predicates as the
platform.
Mock integrations and host actions
Add .maypop/local/mcp.json to exercise MCP discovery, tool wrapping, calls, and
agent tools without invoking a real external integration:
{
"schemaVersion": 1,
"servers": [
{
"id": "search",
"name": "Fixture search",
"url": "mock://search",
"account": "[email protected]",
"tools": [
{
"name": "lookup",
"description": "Return a fixed search fixture.",
"inputSchema": {
"type": "object",
"properties": { "query": { "type": "string" } }
},
"result": {
"content": [{ "type": "text", "text": "Fixture result" }],
"structuredContent": { "items": [] }
}
}
]
}
]
}
maypop.link.to(path) returns a local deep link and maypop.share() opens a
host-owned card containing one. Captured notifications with a path expose an
Open in local app action in the outbox. The local host opens these the way
Maypop does — the app loads at its entry with the path in location.hash and
maypop.launchPath — so an app that routes correctly here routes correctly
once published. None of this claims that a local link was published or that a
captured notification was delivered.
The local bearer token is random for every server run, and a lock prevents two development servers from writing the same data directory. To use a different directory or viewer name:
maypop({
dataDirectory: ".maypop/alice",
username: "Alice",
});
The local host is development tooling, not a security boundary. Keep the development server bound to a trusted interface unless the project itself is safe to expose.
What belongs to this machine lives in .maypop/local/: the local identity,
dev.json, MCP fixtures, KV and Drive data, captured notifications, and the
lock. The development server creates it with its own .gitignore, so it stays
out of Git even when the app's .gitignore does not mention it.
.maypop/kv-policy.json stays beside it and is committed: the platform reads it
from the app's source and applies it with each published version. On its first
run, the server moves what older versions wrote directly into .maypop/ into
.maypop/local/. It leaves an old .maypop/.lock where it is and no longer reads
it; untrack one that was committed with git rm --cached .maypop/.lock.
The lock holds only while the process that wrote it runs. It records that process's PID and start time, so a lock left behind by a server that was killed, or whose sandbox restarted and gave its PID to another process, is taken over. A server that exits without closing still removes its lock.
The bindings only affect framework development servers. Production builds
remain ordinary app bundles. maypop init records the matching build adapter
in maypop.toml, and maypop publish builds and uploads its static output.
Hosted script compatibility
Existing apps can keep loading the global build:
<script src="https://api.dev.maypop.ai/sdk/v1.js"></script>
Importing @basilica-digital/maypop-sdk installs that same build from the package instead. Both
forms use the same host handshake and backend API, and both expose
window.maypop. Do not use both forms in one app; the runtime is idempotent,
but one is enough.
This package is the guest side of the Maypop protocol. Outside Maypop,
maypop.ready() waits for a compatible host. The framework integrations above
provide that host protocol without requiring applications to change their SDK
imports.
Backend artifacts
The published tarball also contains the generated host scripts, lazy capability
chunks, shell document, contracts, and Iroh WebAssembly runtime under dist/.
The Maypop backend pins an exact package version and embeds these files into its
binary, so a backend build never depends on the SDK repository's moving branch.
The source contract and runtime are available through
@basilica-digital/maypop-sdk/source/v1 and
@basilica-digital/maypop-sdk/source/runtime for Maypop's own contract tooling. Application
code should use the public entry points shown above.
Development
All maintained SDK code lives in src/ as TypeScript. pnpm build compiles the
npm entry, hosted global, lazy capability chunks, shell host, injected scripts,
service worker, declarations, and backend artifacts into the ignored dist/
directory. Generated JavaScript is never a source of truth and should not be
edited directly.
The Iroh runtime lives in iroh/; its generated bindings and WebAssembly are
committed under iroh/prebuilt/ so ordinary package builds do not require a
Rust/Wasm toolchain. After changing iroh/src, install the
wasm32-unknown-unknown target, wasm-bindgen-cli matching the crate's pinned
version, and Binaryen's wasm-opt, then run:
just build-iroh
Release
Versions follow SemVer and tags use the matching vX.Y.Z form. Before tagging,
update package.json and the lockfile, then run:
pnpm install --frozen-lockfile
pnpm test
pnpm pack
Pushing the matching tag runs the npm publication workflow. The first release
must be published by an npm owner; after that, configure this repository as the
package's trusted publisher for .github/workflows/release.yml.