# @sylphx/sdk

[![npm version](https://img.shields.io/npm/v/@sylphx/sdk.svg?style=flat)](https://www.npmjs.com/package/@sylphx/sdk)
[![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](https://opensource.org/licenses/MIT)
[![TypeScript](https://img.shields.io/badge/TypeScript-5.0-blue?logo=typescript)](https://www.typescriptlang.org/)
[![docs](https://img.shields.io/badge/docs-sylphx.com%2Fdocs-green)](https://sylphx.com/docs)

Auth, billing, analytics, AI, storage, and more — in one SDK.

📖 **Full docs:** [sylphx.com/docs](https://sylphx.com/docs)

---

## Installation

```bash
npm install @sylphx/sdk
# or
pnpm add @sylphx/sdk
bun add @sylphx/sdk
```

---

## Quick Start (Next.js)

### 1. Environment Variables

Your server and browser connection URLs from your
[Platform Console](https://sylphx.com/console). Sylphx deploys inject these
automatically; local development can pull the same values into `.env.local`.

```bash
# .env.local
SYLPHX_SECRET_URL=sylphx://sk_dev_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx@bold-river-a1b2c3.api.sylphx.com
NEXT_PUBLIC_SYLPHX_URL=sylphx://pk_dev_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx@bold-river-a1b2c3.api.sylphx.com
```

That's it. No other config needed.

> **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.

```ts
// 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:

```tsx
// 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)

```tsx
// 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)

```tsx
'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

```tsx
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

```ts
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 } })
```

### Prefetch App Config

```ts
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.consentTypes
```

### Verify Webhooks

```ts
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:

```ts
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

```ts
import { verifyAccessToken } from '@sylphx/sdk/server'

const payload = await verifyAccessToken(token, {
  secretKey: createServerClient(process.env.SYLPHX_SECRET_URL!).secretKey!,
})
// payload.sub, payload.email, payload.role, payload.app_id
```

---

## React Hooks

### Auth

```tsx
import { useUser, useAuth } from '@sylphx/sdk/react'

const { user, isLoading, isSignedIn } = useUser()
const { signIn, signUp, signOut, forgotPassword } = useAuth()

await signIn({ email: 'user@example.com', password: '...' })
await signOut()
```

### Billing

```tsx
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

```tsx
import { useAnalytics } from '@sylphx/sdk/react'

const { track, identify, page } = useAnalytics()

track('button_clicked', { button: 'upgrade' })
identify({ name: 'John', email: 'john@example.com' })
```

### Feature Flags

```tsx
import { useFeatureFlag } from '@sylphx/sdk/react'

const { isEnabled } = useFeatureFlag('new-dashboard')
if (isEnabled) return <NewDashboard />
```

### AI

```tsx
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

```tsx
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

```tsx
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 { useJobs }          from '@sylphx/sdk/react' // Background jobs
import { useErrorTracking } from '@sylphx/sdk/react' // Error capture
```

---

## UI Components

### Auth

```tsx
import { SignIn, SignUp, UserButton, SignedIn, SignedOut } from '@sylphx/sdk/react'

<SignedOut><SignIn mode="embedded" afterSignInUrl="/dashboard" /></SignedOut>
<SignedIn><UserButton afterSignOutUrl="/" /></SignedIn>
```

### Billing

```tsx
import { PricingTable, CheckoutButton } from '@sylphx/sdk/react'

<PricingTable plans={plans} />
<CheckoutButton planSlug="pro" interval="monthly">Upgrade</CheckoutButton>
```

### Route Protection

```tsx
import { Protect } from '@sylphx/sdk/react'

<Protect role="admin">
  <AdminPanel />
</Protect>
```

---

## Pure Functions (Server or Client)

For non-React environments or maximum control:

```ts
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`](./src/health/README.md) for the full guide.

```ts
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.sock
```

---

## TypeScript

All types are fully inferred. Import them directly:

```ts
import type { User, Plan, Subscription, AppConfig } from '@sylphx/sdk'
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:

```bash
SYLPHX_API_URL=https://platform.your-domain.com sylphx deploy
```

Or in the SDK via an explicit custom-domain connection URL:

```ts
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](../../docs/adr/ADR-083-two-plane-sdk-architecture.md) (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/management`](../management) inherit it. Hand-written shims are a bug.
- **Promise surface, Effect-free.** Internally `@sylphx/contract` is Effect Schema, but the published `.d.ts` is stripped of Effect imports (strip-at-publish CI guard). You get clean `Promise<User>` / `Promise<Plan[]>` signatures with zero Effect dependency in your bundle.
- **Standard Schema compliant.** Every input schema in the contract exposes the `~standard` interface — compatible with Zod, Valibot, ArkType, TypeBox, Effect Schema, and any future Standard-Schema-compliant validator.
- **Agent surface via the CLI.** The `sylphx` CLI is the first-class agent entry point (Claude Code, Cursor, ChatGPT Apps). If you want a custom MCP, compose it on top of `@sylphx/management` yourself — Sylphx does not ship a standalone stdio MCP binary.

See [ADR-084](../../docs/adr/ADR-084-effect-schema-contract.md) for the full design rationale.
