> ## Documentation Index
> Fetch the complete documentation index at: https://docs.pylonsync.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Integrations

> File storage, transactional email, Stripe billing, outbound webhooks, feature flags — how each is wired in Pylon today.

Integrations connect Pylon to the rest of your stack. Some are **built-in subsystems** selected by environment variables (file storage, email); others are **installable TS packages** (Stripe, webhooks, feature flags). None of them are enabled through a `manifest.plugins` list.

## File storage

File storage is a **built-in subsystem** selected by `PYLON_FILES_PROVIDER` — not a plugin entry.

```bash theme={null}
# Local disk (default)
PYLON_FILES_PROVIDER=local            # default
PYLON_FILES_DIR=uploads               # optional, default "uploads/"
PYLON_FILES_URL_PREFIX=/api/files     # optional, default "/api/files"

# Stack0 CDN
PYLON_FILES_PROVIDER=stack0
PYLON_STACK0_API_KEY=sk_live_...      # required for stack0
PYLON_STACK0_FOLDER=uploads           # optional prefix
PYLON_STACK0_BASE_URL=https://...     # optional override

# S3-compatible: AWS S3, Cloudflare R2, Tigris, MinIO, GCS interop
PYLON_FILES_PROVIDER=s3
PYLON_S3_BUCKET=my-bucket             # required
PYLON_S3_ACCESS_KEY=AKIA...           # required
PYLON_S3_SECRET_KEY=...               # required
PYLON_S3_REGION=us-east-1             # optional, default us-east-1
PYLON_S3_ENDPOINT=https://...         # optional; set for R2/Tigris/MinIO (path-style)
PYLON_S3_PUBLIC_URL=https://cdn...    # optional; public/CDN base (public bucket)
PYLON_S3_SESSION_TOKEN=...            # optional; STS/temporary credentials
PYLON_S3_FOLDER=uploads               # optional prefix
```

The server validates these at boot and **fails fast** if a provider is selected without its required vars (e.g. `PYLON_FILES_PROVIDER=s3` without the bucket or keys) — it never silently degrades to local disk. `local`, `stack0`, and `s3` are the providers that ship today.

The `s3` backend uses SigV4 **presigned URLs** for every operation: clients PUT/GET bytes straight to the bucket (never through pylon's memory), and — for a private bucket — pylon tracks per-file ownership so `GET /api/files/<id>` enforces the same owner check as local disk before 302-redirecting to a short-lived presigned URL. Set `PYLON_S3_PUBLIC_URL` to serve from a public bucket/CDN instead. Credentials come from env only (no automatic IAM-role discovery); pass `PYLON_S3_SESSION_TOKEN` alongside temporary credentials.

Uploads use a 3-step direct-to-storage flow so large files never transit the server's memory:

```ts theme={null}
// 1. Ask pylon for an upload slot
const init = await fetch("/api/files/init", {
  method: "POST",
  credentials: "include",
  headers: { "Content-Type": "application/json" },
  body: JSON.stringify({
    filename: file.name,
    mimeType: file.type,
    size: file.size,
  }),
}).then((r) => r.json());

// 2. PUT the bytes DIRECTLY to the returned URL (Stack0's S3 for the
//    stack0 backend, pylon's local-put endpoint for local). Pylon's
//    process is bypassed for the byte transfer.
await fetch(init.uploadUrl, {
  method: "PUT",
  body: file,
  headers: { "Content-Type": file.type },
});

// 3. Confirm so pylon records ownership and returns the canonical URL
const stored = await fetch("/api/files/confirm", {
  method: "POST",
  credentials: "include",
  headers: { "Content-Type": "application/json" },
  body: JSON.stringify({ assetId: init.assetId }),
}).then((r) => r.json());

// stored = { id, url, size }
//   - stack0: url is the CDN URL
//   - local:  url is /api/files/<id> (served by pylon)
```

The shape is identical regardless of backend — clients don't special-case Stack0 vs local. Persist both `stored.url` (display) and `stored.id` (so you can `DELETE /api/files/<id>` later).

* `GET /api/files/<id>` serves the bytes. For CDN backends pylon 302-redirects to the CDN URL; for local it streams from disk.
* `DELETE /api/files/<id>` removes the asset. Owner-gated: only the user who confirmed the upload (or an admin).

<Note>
  The multipart `POST /api/files/upload` was removed in 0.3.91 — it now returns `410 Gone`. Use the 3-step flow above.
</Note>

## Email

Transactional email is a **built-in subsystem** selected by `PYLON_EMAIL_PROVIDER` — not a plugin entry.

```bash theme={null}
PYLON_EMAIL_PROVIDER=sendgrid   # sendgrid | resend | stack0 | webhook
# + the provider's key/config, e.g. SENDGRID_API_KEY / RESEND_API_KEY / PYLON_STACK0_API_KEY
```

Send from an action via `ctx.email`:

```ts theme={null}
import { action, v } from "@pylonsync/functions";

export default action({
  args: { to: v.string() },
  async handler(ctx, args) {
    await ctx.email.send({
      to: args.to,
      subject: "Welcome",
      html: "<p>Thanks for signing up.</p>",
    });
  },
});
```

Auth-flow email (magic codes, verification, invites) uses a separate channel read from `PYLON_AUTH_EMAIL_*`, falling back to `PYLON_EMAIL_*`. This keeps a shared platform auth key out of reach of app code. See [Auth → Email verification](/auth/email-verification).

## Stripe billing

Billing is the installable **[@pylonsync/stripe](/plugins/stripe)** package — plans, checkout, billing portal, signature-verified webhook, and a subscription entity. Full config, hooks, and the webhook endpoint are on its page.

## Outbound webhooks

Firing webhooks at your customers' endpoints is the installable **[@pylonsync/webhooks](/plugins/webhooks)** package — Svix-compatible HMAC signatures, an exponential-backoff retry schedule, secret rotation, and a delivery-audit entity. See its page.

## Feature flags

Runtime feature flags are the installable **[@pylonsync/feature-flags](/plugins/feature-flags)** package — local-eval boolean + multivariate flags, percentage rollouts, targeting predicates. See its page.

## Audit log

Auditing is a **built-in framework feature**, not a plugin: `/api/auth/audit` and `/api/auth/audit/tenant` routes expose the log. Sensitive auth events are recorded automatically.

## Caching

Pylon keeps an in-process cache that backs SSR / ISR response caching. It's internal to the runtime today — there is no user-facing `ctx.cache` API. For app-level memoization, cache inside a server function's own logic or use an external store you call from a function.
