The Slash Socket skill is everything an LLM needs to build with Slash Socket: the manifest rules, the field types and shapes, the client, tokens and every deploy recipe on this site, in one Markdown file. Give it to a model with what you want — "live reactions and a guestbook on my Next.js blog, hosted on Fly" — and it answers with the manifest, the page code, the token endpoint if you need one, and the steps to deploy.
- Chat: copy the skill below into ChatGPT, Claude or any model, then describe what you want.
- Claude Code: save it as .claude/skills/slash-socket/SKILL.md in your project.
- Agents that can fetch: point them at https://slash.tools/socket/skill.md.
SKILL.md
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860---name: slash-socketdescription: Build live features (presence, cursors, chat, counters, polls, reactions, live status) with Slash Socket — writes the socket manifest, the page code, a visitor-token endpoint when needed, and the deploy recipe for the user's host.---
# Slash Socket
You help people add live features to their sites with Slash Socket. A socketis declared in one JSON manifest; the Slash Socket runtime serves exactly thefields it names over a WebSocket and checks every write against them. Peoplerun the runtime themselves — in the Slash Socket app on their Mac, or asslash-socket-server (the Docker image ghcr.io/astrit/slash-socket-server) ontheir own host. slash.tools never carries their traffic.
## What to produce
1. **The manifest** (`socket.json`), starting with `"$schema": "https://slash.tools/socket/v1.json"`. Use the fewest fields that do the job, give every field a `label` and a tight `shape`, and follow the rules below exactly — the runtime refuses anything else and lists why.2. **The page code** with the Slash Socket client, in the user's framework. Import it from their server (`https://their-domain/client.js`) or npm (`@slash.tools/socket`). In React, use the `useField` hook from the docs.3. **A token endpoint** on their backend, only if a field is `signed-in` — HS256 signed with `SOCKET_TOKEN_SECRET`, as in Access & Tokens.4. **The deploy recipe** for where they want it to run. Ask if they didn't say. Recommend a hosted recipe (Fly.io, Railway, Render, VPS + Caddy) for a public https site, the Mac for development. One instance, with a volume.5. **How to load the manifest**: paste it into slash.tools → Dashboard → Socket, import it in the app's Fields tab, push it from the app (Fields → + Server), or pin it with `SOCKET_MANIFEST`.
## Rules
- `name`: 1–40 lowercase letters, digits, hyphens. Field names: a letter, then letters, digits, `_`. At most 32 fields, one `presence`.- Field types: `value` (One shared value), `counter` (A number everyone can bump), `list` (An append-only feed, capped at keep), `map` (One entry per visitor), `stream` (Broadcast only, never stored), `presence` (Who's here, with a little state each).- Only these keys per type: value → shape, initial · counter → initial, min, max, perVisitor · list/map → shape, keep · stream/presence → shape. Every type also takes label, description, read, write, rate.- Cursors, typing and anything sent many times a second are a `stream`, never a list. One-per-visitor things (votes, reactions) are a `map`. A like button is a `counter` with `"perVisitor": 1`.- Anything that must not be gamed (one vote each, posting) is `"write": "signed-in"` with `"auth": { "tokens": true }`; anonymous visitor ids are best effort.- Something only the site owner changes is `"write": "owner"` and is written from their backend through the admin API.- Set `origins` to the user's site origins (plus http://localhost:3000 for development).- Never put `SOCKET_ADMIN_KEY` or `SOCKET_TOKEN_SECRET` in browser code.- https pages need `wss://`. The Mac serves plain ws:// — use a hosted recipe or a tunnel for public https sites.
## Templates
- **Live cursors + chat** — Who's here, everyone's cursor, and a chat.
```json{ "$schema": "https://slash.tools/socket/v1.json", "name": "starter", "title": "Live cursors + chat", "fields": { "presence": { "type": "presence", "label": "Here now", "shape": { "typing": "boolean?" } }, "cursor": { "type": "stream", "label": "Cursors", "shape": { "x": "number(0..1)", "y": "number(0..1)", "text": "string(120)?" } }, "chat": { "type": "list", "label": "Chat", "shape": { "text": "string(500)" }, "keep": 200 } }}```
- **Reactions** — Likes with one per visitor, and emoji reactions.
```json{ "$schema": "https://slash.tools/socket/v1.json", "name": "reactions", "title": "Reactions", "fields": { "likes": { "type": "counter", "label": "Likes", "perVisitor": 1, "min": 0 }, "reaction": { "type": "map", "label": "Reactions", "shape": { "emoji": "enum(👍|❤️|🎉|🔥|👀)" } } }}```
- **Guestbook** — Signed-in visitors leave a note; everyone reads them.
```json{ "$schema": "https://slash.tools/socket/v1.json", "name": "guestbook", "title": "Guestbook", "auth": { "anonymous": true, "tokens": true }, "fields": { "entries": { "type": "list", "label": "Entries", "write": "signed-in", "shape": { "message": "string(280)", "site": "url?" }, "keep": 500, "rate": 0.2 } }}```
- **Live status** — A status line only you set, shown live on your site.
```json{ "$schema": "https://slash.tools/socket/v1.json", "name": "status", "title": "Live status", "fields": { "status": { "type": "value", "label": "Status", "shape": { "text": "string(140)", "emoji": "string(8)?", "busy": "boolean" }, "initial": { "text": "Building things", "busy": false } }, "visitors": { "type": "presence", "label": "Reading now" } }}```
- **Poll** — One vote per visitor, results live.
```json{ "$schema": "https://slash.tools/socket/v1.json", "name": "poll", "title": "Poll", "fields": { "question": { "type": "value", "label": "Question", "shape": { "text": "string(200)", "options": { "a": "string(60)", "b": "string(60)", "c": "string(60)?" } } }, "votes": { "type": "map", "label": "Votes", "shape": { "choice": "enum(a|b|c)" }, "keep": 10000 } }}```
# Reference
The Slash Socket docs (https://slash.tools/socket/docs), in full.
## How It Works
One JSON manifest in, a real-time server out — on your Mac or your own host.
Slash Socket turns a JSON manifest into a real-time server. The manifest names fields — a chat list, a likes counter, who's here, everyone's cursor — and says who may read and write each one. The runtime serves exactly those fields over a WebSocket, checks every write against them, and your page uses them through a small client. There's no server code to write.
- The manifest — one JSON file. Write it on slash.tools under Dashboard → Socket, with the field builder or as JSON, or have an LLM write it with the Slash Socket skill.- The server — the same runtime wherever you want it: inside the Slash Socket app on your Mac, or slash-socket-server on Fly.io, Railway, Render, a VPS, or anything else that runs a container.- The client — your page imports connect from your server's /client.js (or @slash.tools/socket from npm) and reads and writes fields by name.- The app — Slash Socket is the console: every field rendered by type, the pages connected, removing and banning, for your Mac's server or any server you add with its admin key.
`socket.json`
```json{ "$schema": "https://slash.tools/socket/v1.json", "name": "portfolio-live", "title": "Portfolio live", "origins": ["https://astrit.co"], "fields": { "presence": { "type": "presence", "shape": { "page": "string(200)?" } }, "cursor": { "type": "stream", "shape": { "x": "number(0..1)", "y": "number(0..1)" } }, "chat": { "type": "list", "shape": { "text": "string(500)" }, "keep": 200 }, "likes": { "type": "counter", "perVisitor": 1, "min": 0 }, "status": { "type": "value", "shape": { "text": "string(140)" } } }}```
`page.js`
```javascriptimport { connect } from "https://live.astrit.co/client.js"
const socket = connect("wss://live.astrit.co", { name: "Ada" })
socket.field("chat").subscribe((items) => renderChat(items))socket.field("likes").subscribe((count) => (likeButton.textContent = `♥ ${count}`))
likeButton.onclick = () => socket.field("likes").incr()form.onsubmit = () => socket.field("chat").push({ text: input.value })addEventListener("pointermove", (e) => socket.field("cursor").send({ x: e.clientX / innerWidth, y: e.clientY / innerHeight }))```
Your server, your traffic. Visitors connect straight to your server; slash.tools keeps your manifests and your account and is never in the path between your visitors and your socket, so there's no shared server to outgrow and no bill but your own host's. Domains are yours too: point any domain at your server, or let the dashboard publish one through Vercel for a socket running on your Mac.
Next: the Manifest reference, the field types, the client, and a deploy recipe for wherever you want it to run.
## Manifest
Every key a socket manifest takes.
A manifest is one JSON object. name and fields are required; everything else has a sensible default.
`socket.json`
```json{ "$schema": "https://slash.tools/socket/v1.json", "name": "launch-party", "title": "Launch party", "description": "Live reactions and a guestbook for launch day.", "origins": ["https://example.com", "http://localhost:3000"], "auth": { "anonymous": true, "tokens": true }, "limits": { "connections": 5000, "connectionsPerMinute": 30 }, "fields": { "here": { "type": "presence" }, "reactions": { "type": "map", "shape": { "emoji": "enum(🎉|🔥|❤️)" } }, "guestbook": { "type": "list", "write": "signed-in", "shape": { "message": "string(280)" }, "keep": 500 } }}```
- `name` (string, required): The socket's id: 1–40 lowercase letters, digits and hyphens. Data is kept per name, so renaming starts a new socket.- `title` (string): A display name, up to 80 characters.- `description` (string): Up to 500 characters.- `origins` (string[]): Page origins allowed to connect, like https://example.com (scheme and host, no path). Leave it out to allow any page. Browsers always send their origin; scripts can set any, so this guards against other sites, not determined clients.- `auth.anonymous` (boolean): Let pages in without a token. Default true.- `auth.tokens` (boolean): Accept visitor tokens signed by your backend, so those visitors count as signed in. Default false. See Access & Tokens.- `limits.connections` (integer): Open connections at once; 0 for no cap. Defaults to the server's setting (the app's Max Connections).- `limits.connectionsPerMinute` (integer): New connections per IP per minute. Defaults to the server's setting (60).- `fields` (object, required): 1–32 fields, keyed by name: a letter, then letters, digits and _ (up to 40). See Field Types.
Start a manifest with "$schema": "https://slash.tools/socket/v1.json" and editors like VS Code autocomplete and check it as you type. The dashboard, the app and the server each validate it again before using it, and list every problem at once rather than the first.
Changing a manifest is safe. Fields that keep their name and type keep their data (a list is trimmed to a smaller keep, a counter clamped to new bounds); new fields start empty; removed fields are dropped. Every page reconnects to pick up the change — the client does that on its own.
## Field Types
value, counter, list, map, stream and presence — what each holds and what pages can do with it.
Every field has a type. The type decides what the server keeps, what a page can send, and what everyone receives.
- `value` (set): One shared value — a status line, now playing, a page's settings. Pages see it and get every change. Written by the owner by default (write: "owner"); takes shape and initial.- `counter` (incr): A number everyone can bump — likes, votes, visits. incr adds by (default 1, at most ±100 from a page). Takes initial, min, max and perVisitor: how far one visitor's total may move it, so perVisitor: 1 is one like each.- `list` (push): An append-only feed, capped at keep (default 100, up to 10000) — chat, a guestbook, comments. Each item carries an id, a time and who wrote it. The owner can remove items or clear it.- `map` (put · drop): One entry per visitor, keyed by who they are — poll answers, reactions, high scores. put sets your own entry, drop removes it. Capped at keep (default 1000); the oldest entry goes first.- `stream` (send): Broadcast only, never stored — cursors, typing, pointer trails. Delivered to everyone else, dropped for a connection that can't keep up. The client sends at most once per animation frame.- `presence` (set): Who's here, with a little state each — name, page, status. Everyone connected who may write it is a member; set changes your own state. One presence field per manifest.
Keys every field takes:
- `type` (required): One of value, counter, list, map, stream, presence.- `label` (string): What it is, for people — the app and the dashboard show it.- `description` (string): Up to 500 characters.- `read` (anyone · signed-in · owner): Who receives it. Default anyone.- `write` (anyone · signed-in · owner): Who may change it. Default owner for value, anyone for everything else.- `rate` (number): Writes per second per connection (above 0, up to 120). Defaults: stream 60, presence 10, list 2, others 5. A stream quietly drops what's over; other fields answer an error.- `shape` (shape): What a value must look like (value, list, map, stream, presence). See Shapes. Without one, any JSON up to 4 KB is accepted.- `initial` (value · counter): The starting value or number.- `keep` (list · map): How many items or entries are kept.- `min / max` (counter): Bounds the counter is clamped to.- `perVisitor` (counter): How far one visitor's own total may move it, either way.
Who a visitor is: a signed-in visitor is their token's sub; an anonymous one is an id the client keeps in localStorage. Anonymous ids are best effort — someone can clear them — so make anything that matters (one vote each, a guestbook) write: "signed-in".
## Shapes
The small type language that says what a field's values look like.
A shape is a type written as a string, or an object of them. Every value a page sends is checked against its field's shape before it's stored or broadcast, so pages can only ever send what the manifest allows.
- `string` ("string" · "string(140)"): Text, up to 1000 characters or the length given (up to 100000).- `number` ("number" · "number(0..1)"): Any finite number, optionally within a range. Either end may be left open: "number(0..)".- `integer` ("integer" · "integer(1..5)"): A whole number, optionally within a range.- `boolean` ("boolean"): true or false.- `url` ("url"): An http(s) URL up to 2048 characters.- `color` ("color"): #rgb, #rgba, #rrggbb or #rrggbbaa.- `enum` ("enum(a|b|c)"): One of the listed strings (up to 100 options).- `any` ("any"): Any JSON.
- Add ? to make a value optional: "string(80)?" may be left out or null.- An object of shapes describes an object, nested up to four deep. Keys not in the shape are refused.- A field with no shape accepts any JSON up to 4 KB.
`socket.json`
```json{ "cursor": { "type": "stream", "shape": { "x": "number(0..1)", "y": "number(0..1)" } }, "vote": { "type": "map", "shape": { "choice": "enum(yes|no|maybe)" } }, "status": { "type": "value", "shape": { "text": "string(140)", "emoji": "string(8)?", "link": "url?", "busy": "boolean" }, "initial": { "text": "Building things", "busy": false } }}```
## Access & Tokens
Who can read and write each field, and signing visitors in with your own backend.
Each field says who may read it and who may write it: anyone, signed-in visitors, or only the owner. The owner is you — the Slash Socket app and the admin API — never a page.
A visitor is signed in when the page sends a token your backend made: an HS256 JWT signed with the server's token secret, carrying who they are. The server checks it on its own, without calling slash.tools or anyone else. Turn tokens on with "auth": { "tokens": true }.
- `sub` (string, required): The visitor's stable id in your system. Map entries and counter limits use it.- `exp` (number, required): Expiry, in Unix seconds. Keep it short — the client fetches a fresh token on every reconnect.- `name` (string): Shown as the visitor's name (up to 80 characters).- `avatar` (string): An http(s) image URL.
The secret is SOCKET_TOKEN_SECRET on slash-socket-server. On your Mac, Slash Socket makes one for you: Fields → Visitor tokens → Secret copies it, and Test token copies a signed token you can try from a page for an hour. Keep the secret on your backend only.
`app/api/socket-token/route.ts`
```typescriptimport { createHmac } from "node:crypto"
const base64url = (data: string) => Buffer.from(data).toString("base64url")
/** A one-hour Slash Socket token for whoever's signed in to your app. */export async function GET() { const user = await getCurrentUser() // your own auth if (!user) return new Response("Signed out", { status: 401 })
const header = base64url(JSON.stringify({ alg: "HS256", typ: "JWT" })) const payload = base64url( JSON.stringify({ sub: String(user.id), name: user.name, avatar: user.image, exp: Math.floor(Date.now() / 1000) + 60 * 60, }) ) const signature = createHmac("sha256", process.env.SOCKET_TOKEN_SECRET!) .update(`${header}.${payload}`) .digest("base64url") return Response.json({ token: `${header}.${payload}.${signature}` })}```
`page.js`
```javascriptconst socket = connect("wss://live.example.com", { token: async () => (await (await fetch("/api/socket-token")).json()).token,})```
- A token that's wrong or expired closes the connection with code 4001; the client doesn't retry it.- With "anonymous": false, pages without a token can't connect at all.- read: "signed-in" hides a field from anonymous pages entirely — they don't receive its state or its changes.
## Client
Connect a page to a socket and read and write its fields.
Every server serves its client at /client.js, so a page can import it straight from your socket. For bundlers it's also on npm as @slash.tools/socket. No dependencies; it works in browsers and Node 22+.
```bashnpm install @slash.tools/socket```
`page.js`
```javascriptimport { connect } from "https://live.example.com/client.js"// or: import { connect } from "@slash.tools/socket"
const socket = connect("wss://live.example.com", { name: "Ada" })
const chat = socket.field("chat")const off = chat.subscribe((items, event) => render(items))
await chat.push({ text: "hello" }) // resolves when the server accepts itsocket.field("likes").incr()socket.field("presence").set({ page: location.pathname })socket.field("cursor").send({ x: 0.5, y: 0.25 }) // at most once per frame```
- `connect(url, options)` (Socket): Opens the connection and keeps it open, reconnecting with backoff. url can be wss://, https:// or end in /live.- `options` ({ name, avatar, color, token, visitor, reconnect }): How you show up. token is a string or an async function (called on every connect). visitor overrides the anonymous id kept in localStorage; false for none.- `socket.field(name)` (Field): A field by name. Its type, access and state arrive with the welcome.- `field.state`: The current state: value → the value · counter → number · list → items · map → { key: entry } · stream → { connectionId: { from, value, at } } · presence → members. Replaced, never mutated, on each change.- `field.subscribe(fn)` (() => void): Calls fn(state, event) on every change, and straight away once the state is known. Returns an unsubscribe.- `field.listen / field.get`: For React's useSyncExternalStore(field.listen, field.get).- `set · incr · decr · push · put · drop` (Promise): Writes, acknowledged by the server; they reject with a SocketError (code, message) when refused. Writes made while reconnecting wait for the connection.- `send(value)` (void): stream: broadcast to everyone else; calls within one animation frame collapse into the last.- `field.mine()`: map: your own entry, if you've put one.- `socket.me` (Author): Who the server says you are: { id, visitor, name, color, avatar, signedIn }.- `socket.on(event, fn)` (() => void): "status" (connecting · open · closed), "welcome", "error", "message".- `socket.ready` (Promise): Resolves after the first welcome.- `socket.identify({ name, avatar, color, token })`: Reconnects as someone else.- `socket.close()`: Closes for good.
Types: every server describes its own fields as TypeScript at /types.d.ts. Save it next to your code and pass it to connect, and field names, values and state are all checked.
```bashcurl https://live.example.com/types.d.ts -o socket-fields.d.ts```
`socket.ts`
```typescriptimport { connect } from "@slash.tools/socket"import type { Fields } from "./socket-fields"
const socket = connect<Fields>("wss://live.example.com")socket.field("chat").push({ text: "typed" }) // { text: string }```
## React
A useField hook, a chat, and live cursors that don't re-render.
One shared connection per tab, and a hook over any field. Fields replace their state on every change, so useSyncExternalStore re-renders exactly when something changed.
`.env.local`
```bashNEXT_PUBLIC_SOCKET_URL=wss://live.example.com```
`lib/socket.ts`
```tsx"use client"
import { useSyncExternalStore } from "react"import { connect, type Socket } from "@slash.tools/socket"import type { Fields } from "./socket-fields"
let socket: Socket<Fields> | null = null
/** One connection per browser tab, opened on first use. */export function getSocket() { socket ??= connect<Fields>(process.env.NEXT_PUBLIC_SOCKET_URL!) return socket}
export function useField<K extends keyof Fields & string>(name: K) { const field = getSocket().field(name) const state = useSyncExternalStore(field.listen, field.get, () => undefined) return [state, field] as const}```
`components/chat.tsx`
```tsx"use client"
import { useState } from "react"import { useField } from "@/lib/socket"
export function Chat() { const [items, chat] = useField("chat") const [text, setText] = useState("")
return ( <div> {items?.map((item) => ( <p key={item.id}> <b style={{ color: item.by.color }}>{item.by.name}</b> {item.value.text} </p> ))} <form onSubmit={(event) => { event.preventDefault() chat.push({ text }).then(() => setText("")) }} > <input value={text} onChange={(event) => setText(event.target.value)} /> </form> </div> )}```
Cursors move up to 60 times a second per person, so they skip React state: subscribe once and write each cursor's position straight to its element, with transform only and no CSS transition.
`components/cursors.tsx`
```tsx"use client"
import { useEffect, useRef } from "react"import { getSocket } from "@/lib/socket"
export function Cursors() { const layer = useRef<HTMLDivElement>(null)
useEffect(() => { const cursor = getSocket().field("cursor") const move = (event: PointerEvent) => cursor.send({ x: event.clientX / innerWidth, y: event.clientY / innerHeight }) addEventListener("pointermove", move)
const off = cursor.subscribe((frames) => { const root = layer.current if (!root) return for (const [id, frame] of Object.entries(frames)) { let dot = root.querySelector<HTMLElement>(`[data-id="${id}"]`) if (!dot) { dot = document.createElement("div") dot.dataset.id = id dot.className = "cursor" dot.style.background = frame.from.color root.append(dot) } dot.style.transform = `translate(${frame.value.x * innerWidth}px, ${frame.value.y * innerHeight}px)` } for (const dot of root.querySelectorAll<HTMLElement>("[data-id]")) { if (!(dot.dataset.id! in frames)) dot.remove() } })
return () => { removeEventListener("pointermove", move) off() } }, [])
return <div ref={layer} className="cursors" aria-hidden />}```
`cursors.css`
```css.cursors { position: fixed; inset: 0; pointer-events: none; }.cursor { position: absolute; width: 10px; height: 10px; border-radius: 50%; }```
## Admin API
Write fields from your backend, moderate, and push manifests — with the admin key.
Everything the Slash Socket app does to a server, your backend can do too: write fields as the owner (a value only the owner may set, a post from your CMS), clear things, remove connections. Every call carries the server's admin key as a bearer token. Keep that key on servers you control — never in a page.
- `GET /admin/snapshot`: The manifest, every field's full state, the connections, and counts.- `POST /admin/op` ({ op, field, … }): set · incr { by } · push · put { key } · drop { key } · send · remove { id } (a list item) · reset (a counter back to initial or { value }; a list or map cleared; a value back to initial) · kick { id, ban? } · ban { ip } · unban { ip }.- `PUT /admin/manifest` (manifest JSON): Switches the server to another manifest. Refused with 409 when SOCKET_MANIFEST pins it.
```bash# Set a value only the owner may writecurl -X POST https://live.example.com/admin/op \ -H "Authorization: Bearer $SOCKET_ADMIN_KEY" \ -H "Content-Type: application/json" \ -d '{"op":"set","field":"status","value":{"text":"Shipping v2","busy":true}}'
# Clear the chatcurl -X POST https://live.example.com/admin/op \ -H "Authorization: Bearer $SOCKET_ADMIN_KEY" \ -H "Content-Type: application/json" \ -d '{"op":"reset","field":"chat"}'
# Switch to another manifestcurl -X PUT https://live.example.com/admin/manifest \ -H "Authorization: Bearer $SOCKET_ADMIN_KEY" \ -H "Content-Type: application/json" \ --data @socket.json```
The page protocol, for clients in other languages: connect a WebSocket to /live and send JSON text frames. The first is a hello; the server answers with a welcome holding every readable field's state, then pushes changes.
- `hello` (page → server): { op: "hello", name?, avatar?, color?, token?, visitor? } — first, within 10 seconds.- `set · incr · push · put · drop · send` (page → server): { op, field, value?, by?, ref? } — with a ref, the server answers ack { ref } or error { ref, code, message }.- `ping` (page → server): Answered with pong.- `welcome` (server → page): { id, you, server, fields: { name: { type, read, write, keep?, state? } } }- `value · counter · push · remove · reset · put · drop` (server → page): A stored field changed: { op, field, … }.- `stream · join · member · leave` (server → page): A stream frame { from, value }; presence changes { member } or { id }.- `gone` (server → page): A connection closed: { id } — drop its stream frames.- `close codes`: 4000 reconnect (the manifest changed) · 4001 token refused or sign-in required · 4003 removed by the owner · 4008 no hello · 4009 frame over 16 KB.
## Your Mac
Run a socket from the Slash Socket app — great for development and small live sites.
The app is a full server: whatever manifest it holds is served at /live on its port, alongside the classic Socket.IO events. Field data is saved on your Mac and survives restarts.
1. Start the server in Slash Socket. It serves the starter manifest until you give it one.2. Give it a manifest: in the Fields tab, Import JSON… — or define the socket on slash.tools (Dashboard → Socket) with Runs on set to this Mac, and it arrives with the next check-in (Domain → Check Now to fetch it now).3. Connect a page with the import line from Fields → Connect a page.
To reach it from the internet, the Mac needs a way in. Slash Socket offers two, and the Overview's Public access row tells you which link is broken when one isn't working — see Reaching the internet.
Pages served over https can only open wss:// connections. A tunnel gives you that for free; port forwarding needs a certificate, which Slash Socket gets from Let's Encrypt once a domain points at your Mac.
## Docker
slash-socket-server as a container — the base for every other recipe.
slash-socket-server is the same runtime as the app, in a small image: ghcr.io/astrit/slash-socket-server:latest. It serves /live, /client.js, /types.d.ts, /health and the admin API on port 8080.
```bashexport SOCKET_ADMIN_KEY=$(openssl rand -hex 24)echo "$SOCKET_ADMIN_KEY" # keep it — Slash Socket needs it to manage this server
docker volume create socket-datadocker run -d --name socket --restart unless-stopped \ -p 8080:8080 \ -e SOCKET_ADMIN_KEY \ -e SOCKET_TRUST_PROXY=0 \ -v socket-data:/data \ ghcr.io/astrit/slash-socket-server:latest```
To pin a manifest instead of pushing one, mount it and point SOCKET_MANIFEST at it:
`compose.yaml`
```yamlservices: socket: image: ghcr.io/astrit/slash-socket-server:latest restart: unless-stopped ports: ["8080:8080"] environment: SOCKET_ADMIN_KEY: ${SOCKET_ADMIN_KEY} SOCKET_MANIFEST: /config/socket.json volumes: - ./socket.json:/config/socket.json:ro - socket-data:/datavolumes: socket-data:```
- `SOCKET_ADMIN_KEY` (required to manage it): The bearer key for /admin/*. The Slash Socket app and your backend use it. Use 24+ random characters: openssl rand -hex 24.- `SOCKET_MANIFEST` (optional): The manifest, inline JSON or a path to a file. When set, it's pinned. Leave it out to push manifests from the app or the admin API instead (kept in the data directory).- `SOCKET_TOKEN_SECRET` (optional): The secret your backend signs visitor tokens with, for manifests with auth.tokens.- `SOCKET_DATA_DIR` (/data in the image): Where field data and a pushed manifest are kept. Mount a volume here, or everything resets on redeploy.- `SOCKET_ORIGINS` (optional): Comma-separated page origins allowed when the manifest lists none.- `SOCKET_TRUST_PROXY` (1): Read visitors' IPs from X-Forwarded-For — right behind Fly, Railway, Render or Caddy. Set 0 when clients connect straight to the container.- `PORT` (8080): The port to listen on. Most platforms set it for you.
Run one instance. A socket's state lives in that process (and its volume), so two replicas would be two separate sockets. One small instance holds thousands of open connections.
Then manage it from your Mac: in Slash Socket open Fields → + Server, enter its address and admin key, and push a manifest (or set SOCKET_MANIFEST). Pages connect to wss://your-domain with the client from https://your-domain/client.js.
## Fly.io
A socket on Fly.io with a volume and your own domain.
Fly runs the image close to your visitors, keeps WebSockets open and gives you TLS. Save this next to your manifest:
`fly.toml`
```tomlapp = "my-socket"primary_region = "ams"
[build] image = "ghcr.io/astrit/slash-socket-server:latest"
[env] SOCKET_DATA_DIR = "/data"
[[mounts]] source = "socket_data" destination = "/data"
[http_service] internal_port = 8080 force_https = true auto_stop_machines = "off" auto_start_machines = true min_machines_running = 1
[[http_service.checks]] grace_period = "10s" interval = "30s" method = "GET" path = "/health" timeout = "5s"```
```bashfly apps create my-socketfly volumes create socket_data --region ams --size 1fly secrets set SOCKET_ADMIN_KEY=$(openssl rand -hex 24)fly deployfly scale count 1fly certs add live.example.com # then add the DNS records it prints```
auto_stop_machines is off so the socket stays up between visitors — a stopped machine drops every connection. Read the admin key back any time with fly ssh console -C 'printenv SOCKET_ADMIN_KEY'.
Run one instance. A socket's state lives in that process (and its volume), so two replicas would be two separate sockets. One small instance holds thousands of open connections.
Then manage it from your Mac: in Slash Socket open Fields → + Server, enter its address and admin key, and push a manifest (or set SOCKET_MANIFEST). Pages connect to wss://your-domain with the client from https://your-domain/client.js.
## Railway
A socket on Railway from the published image.
1. In Railway, create a project and add a service from a Docker image: ghcr.io/astrit/slash-socket-server:latest.2. Variables: add SOCKET_ADMIN_KEY (Railway's generator or openssl rand -hex 24) and, if you like, SOCKET_MANIFEST with the manifest's JSON.3. Add a volume to the service, mounted at /data.4. Settings → Networking: generate a railway.app domain, or add your own and create the CNAME it shows.5. Keep it at one replica.
Railway sets PORT for you and proxies WebSockets with TLS, so pages use wss://your-domain.
Then manage it from your Mac: in Slash Socket open Fields → + Server, enter its address and admin key, and push a manifest (or set SOCKET_MANIFEST). Pages connect to wss://your-domain with the client from https://your-domain/client.js.
## Render
A socket on Render as a web service with a disk.
Commit this blueprint to a repo and create it in Render under Blueprints → New. A disk needs a paid instance; on the free plan the service sleeps (dropping every connection) and data resets.
`render.yaml`
```yamlservices: - type: web name: my-socket runtime: image image: url: ghcr.io/astrit/slash-socket-server:latest plan: starter healthCheckPath: /health envVars: - key: SOCKET_ADMIN_KEY generateValue: true - key: SOCKET_DATA_DIR value: /data disk: name: socket-data mountPath: /data sizeGB: 1```
Render sets PORT and serves TLS. Copy SOCKET_ADMIN_KEY from the service's Environment page, and add your domain under Settings → Custom Domains.
Then manage it from your Mac: in Slash Socket open Fields → + Server, enter its address and admin key, and push a manifest (or set SOCKET_MANIFEST). Pages connect to wss://your-domain with the client from https://your-domain/client.js.
## VPS + Caddy
Any Linux server with Docker: the socket behind Caddy, with automatic HTTPS.
Point an A record (live.example.com) at the server's IP, then put these two files in one folder on it and run docker compose up -d. Caddy gets a certificate for the domain and passes WebSockets through on its own.
`compose.yaml`
```yamlservices: socket: image: ghcr.io/astrit/slash-socket-server:latest restart: unless-stopped environment: SOCKET_ADMIN_KEY: ${SOCKET_ADMIN_KEY} SOCKET_TOKEN_SECRET: ${SOCKET_TOKEN_SECRET} volumes: - socket-data:/data
caddy: image: caddy:2 restart: unless-stopped ports: ["80:80", "443:443"] volumes: - ./Caddyfile:/etc/caddy/Caddyfile:ro - caddy-data:/data
volumes: socket-data: caddy-data:```
`Caddyfile`
```textlive.example.com { reverse_proxy socket:8080}```
```bashprintf 'SOCKET_ADMIN_KEY=%s\nSOCKET_TOKEN_SECRET=%s\n' $(openssl rand -hex 24) $(openssl rand -hex 32) > .envdocker compose up -dcat .env # the admin key Slash Socket needs```
Then manage it from your Mac: in Slash Socket open Fields → + Server, enter its address and admin key, and push a manifest (or set SOCKET_MANIFEST). Pages connect to wss://your-domain with the client from https://your-domain/client.js.
## Your Domain
Point your own domain at your socket, wherever it runs.
Every socket is on a domain you own. slash.tools never hosts your traffic, so there's nothing to point at us — the record goes straight to your server.
- Fly.io, Railway, Render: add the domain in the platform, then create the CNAME (or A/AAAA) records it shows at your DNS provider. The platform serves TLS.- A VPS: an A record to the server's IP, and Caddy (VPS + Caddy) gets the certificate.- Your Mac: Dashboard → Socket can keep a Vercel domain's A record on your Mac's IP (DNS Forwarding), or use any DNS plus port forwarding — or a tunnel for TLS.
Use wss:// from https pages — browsers block plain ws:// there. Then lock the socket to your site with "origins": ["https://example.com"] in the manifest.
Then paste the manifest into Dashboard → Socket on slash.tools (or import it in the app's Fields tab), and follow the recipe it picked.