The Recordings API enables capturing terminal sessions in asciicast format for playback and sharing.
Recording Architecture
Storage Options
Recordings support three storage backends (priority order):
Cloudflare R2 (Recommended): Global CDN with edge caching
S3-Compatible : Any S3-compatible object storage
PostgreSQL : Database storage (default fallback)
Configuration
R2_ACCOUNT_ID = your-account-id
R2_ACCESS_KEY_ID = your-access-key
R2_SECRET_ACCESS_KEY = your-secret
R2_BUCKET = rexec-recordings
R2_PUBLIC_URL = https://recordings.yourdomain.com
Recordings use the asciicast v2 format :
{ "version" : 2 , "width" : 120 , "height" : 30 , "timestamp" : 1642248000 , "duration" : 45.2 , "title" : "Demo" }
[ 0.123 , "o" , "$ " ]
[ 1.456 , "o" , "ls -la \r\n " ]
[ 1.789 , "o" , "total 24 \r\n drwxr-xr-x..." ]
Each line after header: [time_in_seconds, event_type, data]
Start Recording
Begin recording a terminal session.
curl -X POST https://api.rexec.sh/api/recordings/start \
-H "Authorization: Bearer $TOKEN " \
-H "Content-Type: application/json" \
-d '{
"container_id": "abc123",
"title": "Production Deploy"
}'
POST /api/recordings/start
Request
Container ID to record (Docker ID or name)
Recording title (default: “Recording YYYY-MM-DD HH:MM”)
Response
Resolved Docker container ID
{
"recording_id" : "550e8400-e29b-41d4-a716-446655440000" ,
"started_at" : "2024-01-15T14:30:00Z" ,
"container_id" : "abc123def456" ,
"message" : "Recording started"
}
Error Responses
{
"error" : "already recording this terminal"
}
Stop Recording
Stop recording and save the session.
curl -X POST https://api.rexec.sh/api/recordings/abc123/stop \
-H "Authorization: Bearer $TOKEN "
POST /api/recordings/:containerId/stop
Path Parameters
Container ID (Docker ID or name)
Response
UUID of the saved recording
Recording duration in milliseconds
Human-readable duration (e.g., “2m 35s”)
Total number of recorded events
Public share token (22 characters)
Where recording is stored: “r2”, “s3”, or “database”
Public CDN URL (R2/S3 only)
{
"recording_id" : "550e8400-e29b-41d4-a716-446655440000" ,
"duration_ms" : 155000 ,
"duration" : "2m 35s" ,
"events_count" : 342 ,
"size_bytes" : 45312 ,
"share_token" : "xK9mP2nVqL8sR4tYwZ" ,
"message" : "Recording saved" ,
"storage_type" : "r2" ,
"cdn_url" : "https://recordings.yourdomain.com/550e8400-e29b-41d4-a716-446655440000.cast"
}
Error Responses
{
"error" : "no active recording for this terminal"
}
{
"error" : "not authorized"
}
Get Recording Status
Check if a container is currently being recorded.
GET /api/recordings/:containerId/status
Response
Active Recording
Not Recording
{
"recording" : true ,
"recording_id" : "550e8400-e29b-41d4-a716-446655440000" ,
"started_at" : "2024-01-15T14:30:00Z" ,
"duration_ms" : 65000 ,
"events_count" : 142
}
List Recordings
Retrieve all recordings for the authenticated user.
curl https://api.rexec.sh/api/recordings \
-H "Authorization: Bearer $TOKEN "
GET /api/recordings
Response
Array of recording objects
{
"recordings" : [
{
"id" : "550e8400-e29b-41d4-a716-446655440000" ,
"title" : "Production Deploy" ,
"duration_ms" : 155000 ,
"duration" : "2m 35s" ,
"size_bytes" : 45312 ,
"is_public" : false ,
"share_token" : "xK9mP2nVqL8sR4tYwZ" ,
"share_url" : "/r/xK9mP2nVqL8sR4tYwZ" ,
"created_at" : "2024-01-15T14:30:00Z" ,
"storage_type" : "r2" ,
"cdn_url" : "https://recordings.yourdomain.com/550e8400.cast"
}
]
}
Get Recording
Retrieve metadata for a specific recording.
GET /api/recordings/:id
Path Parameters
Authorization
Requires authentication if recording is private
Public recordings accessible without auth
Response
Same structure as single item in List Recordings.
Get Recording by Token
Retrieve recording metadata using public share token.
GET /api/recordings/shared/:token
Path Parameters
Share token (22 characters)
Response
{
"id" : "550e8400-e29b-41d4-a716-446655440000" ,
"title" : "Production Deploy" ,
"duration_ms" : 155000 ,
"duration" : "2m 35s" ,
"created_at" : "2024-01-15T14:30:00Z" ,
"cdn_url" : "https://recordings.yourdomain.com/550e8400.cast"
}
Stream Recording
Download the recording file (asciicast format).
curl https://api.rexec.sh/api/recordings/550e8400/stream \
-H "Authorization: Bearer $TOKEN " \
-o recording.cast
GET /api/recordings/:id/stream
Response
Content-Type : application/x-asciicast
Content-Disposition : attachment; filename="{title}.cast"
Cache-Control : public, max-age=31536000, immutable
Behavior by Storage Type
Redirects to global CDN URL: 302 Found
Location: https://recordings.yourdomain.com/550e8400.cast
Fetches from S3 and streams through server
Retrieves from PostgreSQL and streams
Stream Recording by Token
Public access to recording file using share token.
GET /api/recordings/shared/:token/stream
Path Parameters
Response
Same as Stream Recording (no authentication required).
Update Recording
Update recording settings (title, visibility).
curl -X PATCH https://api.rexec.sh/api/recordings/550e8400 \
-H "Authorization: Bearer $TOKEN " \
-H "Content-Type: application/json" \
-d '{
"is_public": true,
"title": "Updated Title"
}'
PATCH /api/recordings/:id
Request
Make recording publicly accessible
Response
{
"message" : "recording updated"
}
Delete Recording
Permanently delete a recording.
curl -X DELETE https://api.rexec.sh/api/recordings/550e8400 \
-H "Authorization: Bearer $TOKEN "
DELETE /api/recordings/:id
Behavior
Deletes from external storage (R2/S3) if applicable
Removes database record
Invalidates share token
Response
{
"message" : "recording deleted"
}
Recording Event Capture
The recording system captures three event types:
Output Events
recordingHandler . AddEvent ( containerID , "o" , outputData , 0 , 0 )
Type : "o"
Data : Terminal output bytes (base64 encoded in JSON)
When : Every terminal output chunk
recordingHandler . AddEvent ( containerID , "i" , inputData , 0 , 0 )
Type : "i"
Data : User input
When : User types or pastes
Resize Events
recordingHandler . AddEvent ( containerID , "r" , "" , cols , rows )
Type : "r"
Data : Empty string
Cols/Rows : New terminal dimensions
When : Terminal window resized
Event Structure
type RecordingEvent struct {
Time int64 `json:"t"` // Milliseconds since start
Type string `json:"e"` // "o", "i", or "r"
Data string `json:"d"` // Event data
Cols int `json:"c,omitempty"` // For resize
Rows int `json:"r,omitempty"` // For resize
}
Playback Integration
Asciinema Player
<! DOCTYPE html >
< html >
< head >
< link rel = "stylesheet" type = "text/css" href = "/asciinema-player.css" />
</ head >
< body >
< asciinema-player
src = "https://api.rexec.sh/api/recordings/550e8400/stream"
cols = "120"
rows = "30"
autoplay
loop
></ asciinema-player >
< script src = "/asciinema-player.min.js" ></ script >
</ body >
</ html >
Best Practices
Use R2/S3 storage for production (offloads server bandwidth)
Enable CDN for global playback performance
Set appropriate cache headers (1 year for immutable recordings)
Storage Management
Implement retention policies to delete old recordings
Monitor storage costs (database vs. object storage)
Consider compression for long recordings
Security
Share tokens are cryptographically random (16 bytes base64)
Public recordings accessible without authentication
Private recordings require ownership verification
R2/S3 URLs are public if configured with public bucket
Recording Quality
Typical Sizes : 10-50 KB per minute of terminal activity
Event Count : 100-500 events per minute (varies by activity)
Compression : Asciicast format is already efficient (line-delimited JSON)
Complete Recording Workflow
Start Recording
const { recording_id } = await fetch ( '/api/recordings/start' , {
method: 'POST' ,
body: JSON . stringify ({ container_id: 'abc123' , title: 'Demo' })
}). then ( r => r . json ());
User Interacts
Terminal I/O automatically captured by handler:
Output events: AddEvent(containerID, "o", data, 0, 0)
Resize events: AddEvent(containerID, "r", "", cols, rows)
Stop Recording
const { share_token , cdn_url } = await fetch (
`/api/recordings/ ${ containerID } /stop` ,
{ method: 'POST' }
). then ( r => r . json ());
console . log ( `Share at: /r/ ${ share_token } ` );
Playback
< asciinema-player src = "${cdn_url}" />
Troubleshooting
No Events Recorded
Check container ID resolution:
// Recording handler resolves names to Docker IDs
dockerID := h . resolveContainerID ( containerID )
// Events must use same resolved ID
h . AddEvent ( dockerID , "o" , data , 0 , 0 )
Storage Upload Failures
Verify environment variables: R2_ACCOUNT_ID = abc123
R2_ACCESS_KEY_ID = key
R2_SECRET_ACCESS_KEY = secret
R2_BUCKET = recordings
R2_PUBLIC_URL = https://recordings.example.com
Test connection: r2Store , err := storage . NewR2StoreFromEnv ()
if err != nil {
log . Fatal ( "R2 init failed:" , err )
}
Required IAM permissions: {
"Version" : "2012-10-17" ,
"Statement" : [
{
"Effect" : "Allow" ,
"Action" : [
"s3:PutObject" ,
"s3:GetObject" ,
"s3:DeleteObject"
],
"Resource" : "arn:aws:s3:::bucket-name/*"
}
]
}