@sylphx/sdk
v0.20.1
Published
Sylphx SDK - State-of-the-art platform SDK with pure functions
Readme
@sylphx/sdk
Auth, billing, analytics, AI, storage, and more — in one SDK.
📖 Full docs: sylphx.com/docs
Installation
npm install @sylphx/sdk
# or
pnpm add @sylphx/sdk
bun add @sylphx/sdkQuick Start (Next.js)
1. Environment Variables
Your server and browser connection URLs from your
Platform Console. Sylphx deploys inject these
automatically; local development can pull the same values into .env.local.
# .env.local
SYLPHX_SECRET_URL=sylphx://sk_dev_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx@bold-river-a1b2c3.api.sylphx.com
SYLPHX_URL=sylphx://pk_dev_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx@bold-river-a1b2c3.api.sylphx.com
NEXT_PUBLIC_SYLPHX_URL=sylphx://pk_dev_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx@bold-river-a1b2c3.api.sylphx.comThat's it. No other config needed.
SYLPHX_SECRET_URL is used only on the server for confidential token exchange
and HttpOnly cookie handling. SYLPHX_URL / NEXT_PUBLIC_SYLPHX_URL carry the
publishable credential used by browser-safe auth routes such as
/auth/login, /auth/oauth-providers, passkeys, and OAuth authorize.
Key formats
sylphx://sk_*@<tenant-slug>.api.sylphx.com— Server connection URL (server only, never expose)sylphx://pk_*@<tenant-slug>.api.sylphx.com— Browser connection URL (publishable)Get both from Console → Your App → API Keys. The hosted BaaS API is always addressed as
<tenant-slug>.api.sylphx.com.
2. Middleware
Handles auth routes (/auth/callback, /auth/signout) and route protection automatically.
// middleware.ts
import { createSylphxMiddleware } from '@sylphx/sdk/nextjs'
export default createSylphxMiddleware({
publicRoutes: ['/', '/about', '/pricing', '/login'],
})
export const config = {
matcher: ['/((?!_next|.*\\..*).*)', '/'],
}No manual /api/auth/* routes needed — the middleware handles everything.
Organization context is part of the same contract. Calling the mounted
POST /auth/switch-org route stores the active org in SDK-owned HttpOnly
cookies; subsequent refresh-token rotations restore the org-scoped access token
automatically. If a user belongs to exactly one organization, the middleware
auto-scopes the refreshed session without app-specific glue code.
3. Root Layout
Fetch config server-side once, pass to the provider:
// app/layout.tsx
import { getAppConfig } from '@sylphx/sdk/server'
import { SylphxProvider } from '@sylphx/sdk/react'
import { createServerClient } from '@sylphx/sdk'
export default async function RootLayout({ children }: { children: React.ReactNode }) {
const sylphx = createServerClient(process.env.SYLPHX_SECRET_URL!)
const apiUrl = sylphx.baseUrl.replace(/\/v[0-9]+$/, '')
const config = await getAppConfig({
secretKey: sylphx.secretKey!,
appId: process.env.NEXT_PUBLIC_SYLPHX_APP_ID!,
platformUrl: apiUrl,
})
return (
<html>
<body>
<SylphxProvider
config={config}
appId={process.env.NEXT_PUBLIC_SYLPHX_APP_ID!}
platformUrl={apiUrl}
>
{children}
</SylphxProvider>
</body>
</html>
)
}4. Protect Pages (Server Components)
// app/dashboard/page.tsx
import { currentUser } from '@sylphx/sdk/nextjs'
import { redirect } from 'next/navigation'
export default async function Dashboard() {
const user = await currentUser()
if (!user) redirect('/login')
return <h1>Hello, {user.name}</h1>
}5. Auth UI (Client Components)
'use client'
import { useUser, SignedIn, SignedOut, UserButton } from '@sylphx/sdk/react'
export default function Header() {
const { user } = useUser()
return (
<header>
<SignedOut>
<a href="/login">Sign in</a>
</SignedOut>
<SignedIn>
<span>Hello, {user?.name}</span>
<UserButton afterSignOutUrl="/" />
</SignedIn>
</header>
)
}Server-Side
Get Current User
import { auth, currentUser, currentUserId } from '@sylphx/sdk/nextjs'
// Full auth state
const { userId, user, sessionToken } = await auth()
// Just the user object (null if not signed in)
const user = await currentUser()
// Just the user ID
const userId = await currentUserId()Server API Client
import { createServerClient, getPlans, track } from '@sylphx/sdk'
const sylphx = createServerClient(process.env.SYLPHX_SECRET_URL!)
// Billing
const plans = await getPlans(sylphx)
// Analytics
await track(sylphx, { event: 'purchase', properties: { amount: 99 } })CI Runner Profile Helpers
@sylphx/sdk also exports browser-safe CI runner profile helpers for tools
that generate GitHub Actions workflows:
import { formatCiRunnerWorkflowRunsOn, getCiRunnerProfileById } from '@sylphx/sdk'
const profile = getCiRunnerProfileById('macos-standard')
const runsOn = formatCiRunnerWorkflowRunsOn(profile)
// [self-hosted, sylphx, macos, standard]These helpers expose the public workflow label contract only. Applications do not need to know the backing scheduler, cluster, runner group, capacity formula, or placement details.
Sandbox Capability Catalog
Sandbox clients expose the authenticated capability report served by the exec-server:
import { SandboxClient } from '@sylphx/sdk'
const sandbox = await SandboxClient.create(sylphx)
const report = await sandbox.capabilities.get()
const storage = report.capabilities.find((capability) => capability.id === 'workspace-storage')
if (storage?.workspaceStorage?.pressure === 'critical') {
// Clean caches/artifacts, fork from a clean workspace, or offload heavy work.
}
const mobile = report.capabilities.find((capability) => capability.id === 'mobile-device-test')
if (mobile?.status === 'not-local') {
// Build artifacts in the workspace or CI, then run device tests through the
// managed mobile device runtime when available.
}The report is tenant-neutral and product-level. It tells agents what is local to
the sandbox (exec, files, browser, desktop) versus another Platform execution
plane (ci-runner, managed-device-runtime) without exposing Kubernetes
placement or storage internals.
Prefetch App Config
import {
getAppConfig, // All config in one call (recommended)
getPlans, // Billing plans
getFeatureFlags, // Feature flag definitions
getConsentTypes, // GDPR consent config
} from '@sylphx/sdk/server'
import { createServerClient } from '@sylphx/sdk'
const sylphx = createServerClient(process.env.SYLPHX_SECRET_URL!)
const config = await getAppConfig({
secretKey: sylphx.secretKey!,
appId: process.env.NEXT_PUBLIC_SYLPHX_APP_ID!,
platformUrl: sylphx.baseUrl.replace(/\/v[0-9]+$/, ''),
})
// config.plans, config.featureFlags, config.oauthProviders, config.consentTypesVerify Webhooks
import { verifyWebhook } from '@sylphx/sdk/server'
export async function POST(request: Request) {
const body = await request.text()
const result = await verifyWebhook({
payload: body,
signatureHeader: request.headers.get('x-webhook-signature'),
secret: process.env.SYLPHX_WEBHOOK_SECRET!,
})
if (!result.valid) {
return new Response('Unauthorized', { status: 401 })
}
const { event, data } = result.payload!
// handle event...
return Response.json({ received: true })
}Or use the handler factory:
import { createWebhookHandler } from '@sylphx/sdk/server'
export const POST = createWebhookHandler({
secret: process.env.SYLPHX_WEBHOOK_SECRET!,
handlers: {
'user.created': async (data) => { /* ... */ },
'subscription.updated': async (data) => { /* ... */ },
},
})JWT Verification
import { createServerClient } from '@sylphx/sdk'
import { verifyAccessToken } from '@sylphx/sdk/server'
const sylphx = createServerClient(process.env.SYLPHX_SECRET_URL!)
const payload = await verifyAccessToken(token, {
platformUrl: sylphx.baseUrl.replace(/\/v[0-9]+$/, ''),
audience: process.env.NEXT_PUBLIC_SYLPHX_APP_ID!,
})
// payload.sub, payload.email, payload.role, payload.app_id
// Org-scoped tokens also preserve payload.org_id, payload.org_slug, payload.org_role
// DPoP-bound tokens preserve payload.cnf?.jkt for proof validation.Always pass audience on a resource server. The server verifier rejects a
validly-signed token when its aud claim is for another app or API. Omitting
audience is kept only for backward compatibility and skips audience
validation. If you authorize organization resources, compare payload.org_id
with the addressed resource's org and check payload.org_role before granting
access.
Service Accounts (machine-to-machine)
Authenticate as a service account with the OAuth 2.0 client-credentials
grant (RFC 6749 §4.4, ADR-2062). Returns a short-lived org-scoped access token
carrying org_id / org_slug / org_role claims — verify it on the resource
server with verifyAccessToken (above).
import { createServerClient, signInAsService, withToken } from '@sylphx/sdk'
const sylphx = createServerClient(process.env.SYLPHX_SECRET_URL!)
const { token, expiresIn } = await signInAsService(sylphx, {
clientId: process.env.SYLPHX_SERVICE_CLIENT_ID!, // `sa_…`, stable across rotations
clientSecret: process.env.SYLPHX_SERVICE_CLIENT_SECRET!,
// scope: 'read:foo write:bar', // optional scope downgrade
})
const authed = withToken(sylphx, token)No refresh token is issued — re-mint before expiresIn (seconds) elapses.
Secrets support rotation with a grace window, so a rotated secret keeps
authenticating running consumers until its deadline. org_role is one of
admin / developer / viewer (never owner / super_admin — those stay
human-only).
React Hooks
Auth
import { useUser, useAuth } from '@sylphx/sdk/react'
const { user, isLoading, isSignedIn } = useUser()
const { signIn, signUp, signOut, forgotPassword } = useAuth()
await signIn({ email: '[email protected]', password: '...' })
await signOut()Billing
import { useBilling } from '@sylphx/sdk/react'
const { subscription, isPremium, plans, createCheckout, openPortal } = useBilling()
// Check access
if (!isPremium) return <UpgradePrompt />
// Start checkout
const url = await createCheckout('pro', 'monthly')
window.location.href = url
// Manage subscription
await openPortal()Analytics
import { useAnalytics } from '@sylphx/sdk/react'
const { track, identify, page } = useAnalytics()
track('button_clicked', { button: 'upgrade' })
identify({ name: 'John', email: '[email protected]' })Feature Flags
import { useFeatureFlag } from '@sylphx/sdk/react'
const { isEnabled } = useFeatureFlag('new-dashboard')
if (isEnabled) return <NewDashboard />AI
import { useChat, useCompletion } from '@sylphx/sdk/react'
const { messages, send, isLoading } = useChat({
model: 'anthropic/claude-3.5-sonnet',
})
await send('What is the meaning of life?')Storage
import { useStorage } from '@sylphx/sdk/react'
const { upload, uploadAvatar, isUploading, progress } = useStorage()
const url = await upload(file, { path: 'documents/' })
const avatarUrl = await uploadAvatar(imageFile)More Hooks
import { useConsent } from '@sylphx/sdk/react' // GDPR consent
import { useFeatureFlags } from '@sylphx/sdk/react' // All flags at once
import { useNotifications } from '@sylphx/sdk/react' // In-app notifications
import { useReferral } from '@sylphx/sdk/react' // Referral program
import { useOrganization } from '@sylphx/sdk/react' // Multi-tenant orgs
import { useTasks } from '@sylphx/sdk/react' // Background tasks
import { useErrorTracking } from '@sylphx/sdk/react' // Error captureUI Components
Auth
import { SignIn, SignUp, UserButton, SignedIn, SignedOut } from '@sylphx/sdk/react'
<SignedOut><SignIn mode="embedded" afterSignInUrl="/dashboard" /></SignedOut>
<SignedIn><UserButton afterSignOutUrl="/" /></SignedIn>Billing
import { PricingTable, CheckoutButton } from '@sylphx/sdk/react'
<PricingTable plans={plans} />
<CheckoutButton planSlug="pro" interval="monthly">Upgrade</CheckoutButton>Route Protection
import { Protect } from '@sylphx/sdk/react'
<Protect role="admin">
<AdminPanel />
</Protect>Pure Functions (Server or Client)
For non-React environments or maximum control:
import { createClient, signIn, track, getPlans } from '@sylphx/sdk'
const config = createClient(process.env.NEXT_PUBLIC_SYLPHX_URL!)
// Auth
const tokens = await signIn(config, { email, password })
const authedConfig = withToken(config, tokens.accessToken)
// Analytics
await track(config, { event: 'purchase', properties: { amount: 99 } })
// Billing
const plans = await getPlans(config)Entry Points
| Import path | Use for |
|---|---|
| @sylphx/sdk | Pure functions (server or client, no React) |
| @sylphx/sdk/react | React hooks, components, SylphxProvider |
| @sylphx/sdk/server | JWT verification, webhook verification, server client |
| @sylphx/sdk/nextjs | createSylphxMiddleware, auth(), currentUser() |
| @sylphx/sdk/web-analytics | Standalone web-analytics tracker (rrweb + web-vitals) |
| @sylphx/sdk/health | Multi-signal health score for the sylphx-health-agent sidecar (ADR-111 Phase B) |
@sylphx/sdk/health — Phase B health score (ADR-111)
Apps register signals (event-loop lag, queue depth, error rate, memory pressure)
and the SDK folds them into a continuous score in [0, 1]. The
sylphx-health-agent sidecar polls the score and decides liveness /
readiness / drain via the three-tier gate. See
src/health/README.md for the full guide.
import { sylphxHealth, eventLoopLagSignal, queueDepthSignal } from '@sylphx/sdk/health'
const health = sylphxHealth({
signals: [
eventLoopLagSignal({ degradedMs: 5000, deadMs: 30000 }),
queueDepthSignal({ getter: () => queue.size, fullThreshold: 1000 }),
],
})
app.get('/healthz', health.handler())
// Or — Unix-socket transport for the sidecar:
health.serveUnixSocket() // → /var/run/sylphx/health.sockTypeScript
All types are fully inferred. Import them directly:
import type { User, Plan, Subscription } from '@sylphx/sdk'
import type { AppConfig } from '@sylphx/sdk/server'
import type { AuthResult } from '@sylphx/sdk/nextjs'Self-Hosting / Custom Deployment
If you're running your own Sylphx Platform deployment, configure the base URL via the CLI:
SYLPHX_API_URL=https://platform.your-domain.com sylphx deployOr in the SDK via an explicit custom-domain connection URL:
import { createServerClient } from '@sylphx/sdk'
const config = createServerClient(
'sylphx://sk_prod_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx@acme.api.example.com',
)Note: hosted Sylphx uses
<tenant-slug>.api.sylphx.com. Only use a custom host for self-hosted deployments or a documented legacy migration.
Architecture — contract-first
This SDK is part of the two-plane architecture (ADR-083) with a single source of truth (ADR-084):
@sylphx/contract(Effect Schema) drives the types you see here. The types are not hand-written and not separately maintained — add an endpoint to the contract and both this SDK and the sibling@sylphx/managementinherit it. Hand-written shims are a bug.- Promise surface, Effect-free. Internally
@sylphx/contractis Effect Schema, but the published.d.tsis stripped of Effect imports (strip-at-publish CI guard). You get cleanPromise<User>/Promise<Plan[]>signatures with zero Effect dependency in your bundle. - Standard Schema compliant. Every input schema in the contract exposes the
~standardinterface — compatible with Zod, Valibot, ArkType, TypeBox, Effect Schema, and any future Standard-Schema-compliant validator. - Agent surface via the CLI. The
sylphxCLI is the first-class agent entry point (Claude Code, Cursor, ChatGPT Apps). If you want a custom MCP, compose it on top of@sylphx/managementyourself — Sylphx does not ship a standalone stdio MCP binary.
See ADR-084 for the full design rationale.
