diff --git a/docs-src/spectrum-ts/content.mdx.vel b/docs-src/spectrum-ts/content.mdx.vel
index ad98706..1cb7a65 100644
--- a/docs-src/spectrum-ts/content.mdx.vel
+++ b/docs-src/spectrum-ts/content.mdx.vel
@@ -29,7 +29,7 @@ Use this section when you need to choose the right outbound content shape or und
Share contact cards from structured data, users, or vCards.
- Render URLs as rich previews with lazy Open Graph metadata.
+ Send URLs as rich preview cards rendered by the receiving platform.
Present URLs as tappable app-style cards where providers support them.
diff --git a/docs-src/spectrum-ts/content/rich-links.mdx.vel b/docs-src/spectrum-ts/content/rich-links.mdx.vel
index 3e4fcc7..fa04518 100644
--- a/docs-src/spectrum-ts/content/rich-links.mdx.vel
+++ b/docs-src/spectrum-ts/content/rich-links.mdx.vel
@@ -1,9 +1,9 @@
---
title: "Rich links"
-description: "Render URLs as rich previews with lazy Open Graph metadata."
+description: "Send URLs as rich preview cards rendered by the receiving platform."
---
-Use `richlink()` to render a URL as a rich preview card with title, summary, and cover image. Spectrum scrapes Open Graph metadata at send time. Pass just the URL and the builder fills in the rest.
+Use `richlink()` to send a URL as a rich preview card. Each platform's native client renders the preview — iMessage enables `enableLinkPreview`, Telegram auto-unfurls the bare URL. Spectrum carries only the URL; metadata fetching is left to the platform.
```ts
import { richlink } from "spectrum-ts";
@@ -11,13 +11,13 @@ import { richlink } from "spectrum-ts";
await space.send(richlink("https://example.com/article"));
```
-`title()`, `summary()`, and `cover()` are lazy async accessors. The metadata fetch happens only if the receiving platform needs it. Platforms without rich-link support fall back to the URL as plain text.
+Platforms without rich-link support fall back to the URL as plain text.
-
+`richlink` is **outbound-only**. Inbound URLs always arrive as `text` content — a URL received from any platform is normalized to its URL string and deserialized as plain `text`, never as a `richlink` payload.
+
+
| Field | Type | Description |
|---|---|---|
- | `url` | `string` | The original URL. |
- | `title()` | `() => Promise` | OG title. |
- | `summary()` | `() => Promise` | OG description. |
- | `cover()` | `() => Promise<{ mimeType?, read(), stream() } \| undefined>` | OG image. |
+ | `type` | `"richlink"` | Content discriminator. |
+ | `url` | `string` | The URL to preview. |
diff --git a/docs-src/spectrum-ts/introduction.mdx.vel b/docs-src/spectrum-ts/introduction.mdx.vel
index 30d16d6..2e52f94 100644
--- a/docs-src/spectrum-ts/introduction.mdx.vel
+++ b/docs-src/spectrum-ts/introduction.mdx.vel
@@ -116,7 +116,7 @@ You do not have to manage Spectrum only from the dashboard. Use the dashboard fo
Install `spectrum-ts` and send your first message.
- Receive messages via HTTP with native and Fusor webhook support, plus Hono, Express, and Elysia adapters.
+ Receive messages via HTTP with native and Fusor webhook support, plus Hono, Express, Elysia, and Fastify adapters.
Add a new interface with Spectrum's provider model.
diff --git a/docs-src/spectrum-ts/webhooks.mdx.vel b/docs-src/spectrum-ts/webhooks.mdx.vel
index 243f9a5..3e3e8a9 100644
--- a/docs-src/spectrum-ts/webhooks.mdx.vel
+++ b/docs-src/spectrum-ts/webhooks.mdx.vel
@@ -168,9 +168,39 @@ First-party adapters mount the endpoint for you and handle raw-body parsing corr
.listen(3000);
```
+
+ ```ts
+ import Fastify from "fastify";
+ import { Spectrum } from "spectrum-ts";
+ import { imessage } from "spectrum-ts/providers/imessage";
+ import { spectrum } from "@spectrum-ts/fastify";
+
+ const app = await Spectrum({
+ projectId: process.env.PROJECT_ID!,
+ projectSecret: process.env.PROJECT_SECRET!,
+ providers: [imessage.config()],
+ webhookSecret: process.env.SPECTRUM_WEBHOOK_SECRET,
+ });
+
+ const server = Fastify();
+
+ server.register(spectrum, {
+ app,
+ onMessage: async (space, message) => {
+ if (message.content.type === "text") {
+ await space.send(`echo: ${message.content.text}`);
+ }
+ },
+ });
+
+ await server.listen({ port: 3000 });
+ ```
+
+ Fastify auto-parses known content types before your route handler runs, which breaks HMAC verification. The `@spectrum-ts/fastify` plugin registers a wildcard raw-body parser in its own encapsulated scope, so the webhook route receives exact wire bytes while your other routes keep their normal JSON parsing.
+
-All three adapters accept the same options:
+All four adapters accept the same options:
| Option | Type | Default | Description |
|---|---|---|---|
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 11734b2..51e33cb 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -2341,8 +2341,8 @@ packages:
ms@2.1.3:
resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==}
- nanoid@3.3.14:
- resolution: {integrity: sha512-U9kYi5bpVMEI31yC8iw4bJJp0avcHXA0W8/wNfLfnvJYzihQo2ZRPYPvpAAd570HAcCBjCTN7vnr+v4StKl1IQ==}
+ nanoid@3.3.15:
+ resolution: {integrity: sha512-y7Wygv/7mEOvxTuEQDB8StXdMRBWf1kR/tlhAzBRUFkB2jfcLOAxO/SHmOO2zgz1pVgK29/kyupn059/bCHdjA==}
engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1}
hasBin: true
@@ -3704,8 +3704,8 @@ snapshots:
'@typescript-eslint/project-service@8.58.1(typescript@5.9.3)':
dependencies:
- '@typescript-eslint/tsconfig-utils': 8.58.2(typescript@5.9.3)
- '@typescript-eslint/types': 8.58.2
+ '@typescript-eslint/tsconfig-utils': 8.58.1(typescript@5.9.3)
+ '@typescript-eslint/types': 8.58.1
debug: 4.4.3
typescript: 5.9.3
transitivePeerDependencies:
@@ -5217,7 +5217,7 @@ snapshots:
ms@2.1.3: {}
- nanoid@3.3.14: {}
+ nanoid@3.3.15: {}
napi-build-utils@2.0.0:
optional: true
@@ -5404,7 +5404,7 @@ snapshots:
postcss@8.5.15:
dependencies:
- nanoid: 3.3.14
+ nanoid: 3.3.15
picocolors: 1.1.1
source-map-js: 1.2.1