# tail.surf

> Durable, shareable streams. Pipe anything in — process output, logs, structured events, arbitrary bytes — follow it live from a browser or terminal, replay the full history. Transcript and byte streams are append-only logs. Terminal sessions use separate input and output logs. All have a stable URL, an optional title, and scoped share links. Built on S2 (s2.dev).

Hosted streams expire after 10 days by default. Records are immutable; an owner can delete the whole stream, but nobody can edit or remove one record.

## URLs and auth

- Web console: https://tail.surf/s/{stream_id}
- Web terminal: https://tail.surf/t/{stream_id}
- The stream console URL is curlable. Transcript streams return `text/plain`. Byte streams return `application/octet-stream`. Append `?wait=60` to follow long-poll style: the read ends once that many seconds pass at the tail with no new record (max 60).
- Explicit plain view: https://tail.surf/s/{stream_id}/raw (accepts `?wait=`, `?seq_num=`, `?tail_offset=`)
- REST base: https://tail.surf/api/v1
- Authorization is a per-stream link secret sent as `Authorization: Bearer {secret}`. Link permissions: `o` (owner), `r` (read), `w` (write), `rw`.
- Web share links carry the secret in the URL fragment, e.g. https://tail.surf/s/{stream_id}#r={secret}. Fragments never reach the server; use the Bearer header for HTTP requests.
- Public streams are readable with no credential. Writing always requires a write-capable secret.

## Core HTTP API

Create a stream. No auth. The response includes the immutable `kind` and minted link secrets. Kind is `transcript`, `bytes`, or `terminal`. It defaults to `transcript`.

```
POST /api/v1/streams
{"visibility":"public","title":"deploy log","links":[{"link_id":"owner","permissions":"o"}]}
```

Append records with a write-capable secret. A bare records body is a one-shot append; a retry may append a duplicate.

```
POST /api/v1/streams/{stream_id}/records
Authorization: Bearer {secret}
{"records":[{"text":"hello"}]}
```

For deduplicating retries, name your writer: add `"writer":{"id":"{16 bytes as 22 base64url chars}","seq_num":"0"}`, incrementing `seq_num` per record. A retry with the same identity, sequence, and data is suppressed by readers.

A record's payload sits under one key: `text` carries UTF-8 directly and `bytes` carries base64url. These are JSON encodings. The stream kind defines semantics. A record is at most 512 KiB; a batch carries up to 128 records.

Read as server-sent events. Start with one of `seq_num`, `timestamp`, or `tail_offset` (default `tail_offset=0`, the live tail). `seq_num=0` replays everything, then follows. Also `count`, `until`, `rate`, and `wait`. Event ids are resumable cursors for `Last-Event-ID`. A `read_batch` record looks like `{"seq_num":"0","timestamp_ms":"...","writer":{"id":"...","seq_num":"0"},"text":"hello"}`.

```
GET /api/v1/streams/{stream_id}/records?seq_num=0
Accept: text/event-stream
```

Metadata and management:

- `GET /api/v1/streams/{stream_id}` — metadata; read permission for private streams
- `PATCH /api/v1/streams/{stream_id}` — update `title`, `visibility`, or `expires_at`; owner
- `DELETE /api/v1/streams/{stream_id}` — delete the stream; owner
- `PUT /api/v1/streams/{stream_id}/links/{link_id}` — mint a link with `permissions` and optional `expires_at`; owner
- `GET /api/v1/streams/{stream_id}/links` — list links; owner
- `DELETE /api/v1/streams/{stream_id}/links/{link_id}` — revoke a link; owner

A `link_id` is a client-chosen name: 1 to 64 lowercase letters, digits, or hyphens. Sequence numbers and millisecond timestamps are decimal strings in JSON bodies.

## CLI

```
curl --proto '=https' --tlsv1.2 -LsSf https://tail.surf/install | sh
anything | tsf                    # create a stream and pipe into it
anything | tsf write '{link}'     # append to an existing stream
tsf tail '{url}'                  # follow
tsf replay '{url}'                # print history, stop at the tail
```

## Docs

- [Protocol](https://github.com/s2-streamstore/tailsurf/blob/main/docs/protocol.md): the full REST, SSE, and WebSocket contract
- [OpenAPI](https://github.com/s2-streamstore/tailsurf/blob/main/docs/openapi.yaml): machine-readable REST and SSE schemas
