Live streams
Historian: your own storage
Keep recordings in your own S3-compatible bucket (AWS, R2, B2, Wasabi, Hetzner, OVH, or MinIO on site): key, CORS, lifecycle, setup and health.
By default the recordings of sources that record always or by trigger are stored by Kymoscope. An organization can instead keep their day files in its own S3-compatible bucket, in the cloud or on site. This page is for the IT administrator who prepares the bucket and the organization admin who connects it in the app. Nothing changes for the people who chart history: the same days open the same way.
When to use it
- Data sovereignty: the files stay in a bucket, a region or a building you choose.
- Your own retention rules or a write-once (WORM) policy on top of ours.
- Very long histories on storage you already pay for.
Day files are encrypted on the collector before they leave it, so your bucket holds only ciphertext, exactly as Kymoscope storage does. Kymoscope keeps only the index: which day, which file, which version. Your own storage is part of Enterprise; ask Kymoscope to turn it on for your organization.
What you need
An S3-compatible bucket reachable over HTTPS, with a certificate the browsers trust, from two places: your collectors (they upload) and the browsers of the people who open history (they read day files straight from the bucket). Kymoscope itself never connects to it.
| Provider | Endpoint and region |
|---|---|
| AWS S3 | https://s3.<region>.amazonaws.com, region for example eu-central-1 |
| Cloudflare R2 | https://<account>.eu.r2.cloudflarestorage.com, region auto |
| Backblaze B2 | https://s3.<region>.backblazeb2.com, region for example eu-central-003 |
| Wasabi | https://s3.<region>.wasabisys.com, region for example eu-central-1 |
| Hetzner Object Storage | https://<location>.your-objectstorage.com, region = the location, for example fsn1 |
| OVHcloud | https://s3.<region>.io.cloud.ovh.net, region for example gra |
| On site: MinIO, Garage, SeaweedFS or Ceph RGW | https://minio.customer.local:9000, region us-east-1 unless you set another |
Set the region. Requests are signed for the region you enter, and it defaults to us-east-1. On AWS, a bucket in any other region then refuses every request with a signature error: enter the bucket's own region, for example eu-central-1.
The endpoint is https://host or https://host:port with no path. The optional prefix (for example kymoscope/) puts every Kymoscope file under one folder of the bucket.
A key with the least rights
Create a key used only by Kymoscope. On AWS, attach this policy to its user (replace customer-history and kymoscope/ with your bucket and prefix):
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "KymoscopeDayFiles",
"Effect": "Allow",
"Action": [
"s3:PutObject",
"s3:GetObject",
"s3:DeleteObject",
"s3:AbortMultipartUpload",
"s3:ListMultipartUploadParts"
],
"Resource": "arn:aws:s3:::customer-history/kymoscope/*"
},
{
"Sid": "KymoscopeBucket",
"Effect": "Allow",
"Action": ["s3:ListBucket", "s3:GetLifecycleConfiguration"],
"Resource": "arn:aws:s3:::customer-history"
}
]
}s3:ListBucketlets a file that is really missing answer 404 instead of 403, so a deleted file reads as a gap and a permission problem reads as one.s3:GetLifecycleConfigurationlets the setup check warn about missing lifecycle rules. Without it the check says "unknown" and still passes.- Cloudflare R2: an API token with "Object Read & Write" for this bucket only.
- Backblaze B2: an application key restricted to this bucket (and prefix), with read and write.
- MinIO and other on-site stores: a user with a policy of the same actions (see On site below).
The secret access key is entered once in the app and stored encrypted. Nobody can read it back, Kymoscope staff included; to change it, enter a new one.
CORS
Browsers read history straight from your bucket, so the bucket must allow the app's origin. The app shows this rule with a Copy button:
[
{
"AllowedOrigins": ["https://app.kymoscope.com"],
"AllowedMethods": ["GET", "HEAD", "PUT", "DELETE"],
"AllowedHeaders": ["*"],
"ExposeHeaders": ["ETag", "Content-Length", "Content-Range"],
"MaxAgeSeconds": 3600
}
]{"rules":[{"allowed":{"origins":["https://app.kymoscope.com"],"methods":["GET","HEAD","PUT","DELETE"],"headers":["*"]},"exposeHeaders":["ETag","Content-Length","Content-Range"],"maxAgeSeconds":3600}]}# Cloudflare R2 (cors.json holds the R2 rule above)
npx wrangler r2 bucket cors set customer-history --file cors.jsonMinIO allows every origin by default. To narrow it, run mc admin config set <alias> api cors_allow_origin=https://app.kymoscope.com.
PUT and DELETE are there for the setup check, which runs in the browser. They open nothing: every request still needs a short-lived link signed by Kymoscope for your key.
Lifecycle
- Add "abort incomplete multipart uploads after 7 days". An upload interrupted for good leaves parts behind; this rule cleans them. The setup check warns while it is missing.
- Don't expire objects sooner than your raw data years, and not sooner than the 10 years of the 1-minute and 1-hour summaries. Expired files become gaps. Your collectors already delete expired and replaced files on schedule, so no expiration rule is needed.
- Infrequent-access tiers are fine (S3 Standard-IA, Wasabi, B2). Archive tiers that need a restore (Glacier Flexible Retrieval, Deep Archive, Cold Archive) are not: reads of those days would fail.
- WORM (Object Lock): the setup check shows whether the bucket accepts Kymoscope's writes. Files under retention can't be deleted when a day is replaced; the collector retries every hour until the retention ends.
{
"Rules": [
{
"ID": "kymoscope-abort-incomplete-uploads",
"Status": "Enabled",
"Filter": { "Prefix": "kymoscope/" },
"AbortIncompleteMultipartUpload": { "DaysAfterInitiation": 7 }
}
]
}Set it up in the app
- Update every collector of the organization to kymoscope-collector 0.5.0 or later. Older collectors can't upload to your bucket and wait with
collector_update_needed. - As an organization owner or admin, open Organization → Storage.
- Fill in Endpoint, Region, Bucket, Prefix (optional), Access key ID and Secret access key, then click Save bucket. From now on new days wait on your collectors until the bucket passes its test (or you click Use anyway): once a bucket is saved, they never go to Kymoscope storage.
- Click Test bucket. Your browser writes a small test file, reads part of it back (a range read, as charts do), deletes it and reads the lifecycle rules. When write, read, range and delete pass, new days go to your bucket.
Use anyway appears after a failed test. It is for a bucket this browser can't reach but your collectors and readers can, for example an on-site MinIO tested from outside the customer network. You decide; nothing is held back.
Lifecycle findings are warnings and never stop the test. Days stored before the switch stay where they are and remain readable.
One collector that doesn't follow
Every collector follows the organization's choice unless its collector.yaml sets recording.where:
recording:
where: kymoscope # kymoscope: Kymoscope storage, even with a bucket saved
# own: the organization's bucket; omit to follow the organizationWith where: own and no bucket saved, new days wait on the collector, and the log says so (no_own_storage), until an admin adds one in Organization → Storage.
On site
MinIO (or Garage, SeaweedFS, Ceph RGW) can run next to your own relay, inside the customer network. Kymoscope never connects to it: only your collectors and your browsers do. The server only signs short-lived links for it.
# public.crt and private.key in /srv/minio-certs: a certificate your browsers trust
docker run -d --name minio --restart unless-stopped -p 9000:9000 \
-v /srv/minio:/data -v /srv/minio-certs:/certs \
-e MINIO_ROOT_USER=customer-admin -e MINIO_ROOT_PASSWORD='<a long random secret>' \
pgsty/minio:RELEASE.2026-08-04T00-00-00Z server /data --certs-dir /certs
mc alias set customer https://minio.customer.local:9000 customer-admin '<a long random secret>'
mc mb customer/customer-history# MinIO: allow the app's origin (MinIO allows every origin by default)
mc admin config set customer api cors_allow_origin=https://app.kymoscope.com
# a user with the same rights as the IAM policy above
mc admin policy create customer kymoscope-history policy.json
mc admin user add customer kymoscope-history '<a long random secret>'
mc admin policy attach customer kymoscope-history --user kymoscope-history
# MinIO removes incomplete uploads on its own (api stale_uploads_expiry, 24h by default)
mc admin config get customer api- Use a TLS certificate your browsers trust (your internal CA rolled out to the workstations is fine). Browsers refuse plain
http://storage from the app. - Viewers outside the customer network can't open those days: the chart says "Storage unreachable from this network". Everything else in the app keeps working for them.
- Chrome may ask once to allow the app to access devices on your local network. Allow it for the app.
Health
The Storage tab shows the last test and what failed, the last upload, and the last storage error a collector reported. On the collector, the health stream carries, per source that records by itself:
| Channel | With your own storage |
|---|---|
<source>.history_days_pending | Closed days not uploaded yet. Rises while the bucket is unreachable or refuses the key; 0 or 1 most of the time. |
<source>.history_errors | Failed upload attempts since start, including refusals by your bucket. The collector log names the step and the status code. |
<source>.history_storage_waiting | 1 while new days wait because the bucket hasn’t passed its test yet (storage_check_pending), recording.where is own and no bucket is saved (no_own_storage), or your own-storage contract has ended (customer_storage_not_in_plan). The collector log says what an admin must do. |
Changing or removing the bucket
- A change applies to new days only. Days already stored stay in the bucket they were written to, so keep the old bucket and its key valid until those days expire. After saving another bucket, new days wait on the collectors until it passes its test or an admin clicks Use anyway.
- Rotating the key of the same bucket: enter the new key in the Storage tab and test again. Every stored day stays readable with the new key. This works even after your own-storage contract has ended, so stored days stay readable.
- Stop using this bucket sends new days back to Kymoscope storage. Days already in your bucket stay there and stay readable.
- Replaced and expired files in your bucket (a day rewritten with late data, a day past its retention) are deleted by your collectors, once an hour. Kymoscope can't reach your bucket to do it.
- If the contract for your own storage ends while a bucket is saved, new days wait on the collectors (
history_days_pendingrises) until it is renewed or an admin clicks Stop using this bucket. Nothing is moved to Kymoscope storage, and stored days stay readable.
If you delete files yourself
A day whose file was deleted from your bucket shows as a gap in charts, never as an error. If late data arrives for that day, the collector rebuilds it from the values it still holds locally. An export leaves that day out and names it as missing from your storage.
A 403 is not a missing file. When your bucket refuses the collector's key while it reads a day back (to merge late data), the day waits and history_errors rises, with a permission problem in the log. Nothing is rebuilt from partial data. Fix the key's rights, and include s3:ListBucket so that a file that is really gone answers 404.
Troubleshooting
| Message | What to do |
|---|---|
customer_storage_not_in_plan | Own storage is not on for your organization (or its end date has passed) while a bucket is saved. New days wait on the collector. Ask Kymoscope to renew it, or click Stop using this bucket to store new days with Kymoscope. |
no_own_storage | The collector has recording.where: own but the organization has no bucket. New days wait on the collector. An organization admin adds one in Organization → Storage, or set where: kymoscope. |
storage_check_pending | A bucket is saved but hasn’t passed its test yet. New days wait on the collector. An organization admin must test the bucket in Organization → Storage (or click Use anyway). |
collector_update_needed | This collector is older than 0.5.0. Update it; the day it was uploading starts again cleanly. |
| write / read / delete: blocked (CORS, TLS or network) | The browser could not talk to the bucket at all: no CORS rule for https://app.kymoscope.com, a certificate the browser does not trust, plain http://, or a host not reachable from this network. |
| write / read / delete: 403 | The key lacks a right, or the bucket name, prefix or region is wrong. Compare with the policy above. |
| A signature error (SignatureDoesNotMatch, AuthorizationHeaderMalformed) | The region is wrong. On AWS enter the bucket’s own region; us-east-1 is only the default. |
| range: the bucket ignored the Range header or hides Content-Range | Add Content-Range (and Content-Length, ETag) to ExposeHeaders in the CORS rule. |
| lifecycle: none | Warning only: add the abort-incomplete-uploads rule. On MinIO you can ignore it: MinIO removes incomplete uploads itself. |
| Storage unreachable from this network (in a chart) | The viewer’s network can’t reach the bucket, typically an on-site store opened from outside the customer network. |
Automation
Prepare the bucket from scripts with the files above (policy.json, cors.json, lifecycle.json):
# AWS CLI: CORS (cors.json holds the S3 rule above, wrapped as {"CORSRules": [...]})
aws s3api put-bucket-cors --bucket customer-history --cors-configuration file://cors.json
# lifecycle: abort incomplete uploads after 7 days (lifecycle.json above)
aws s3api put-bucket-lifecycle-configuration --bucket customer-history \
--lifecycle-configuration file://lifecycle.json
# the least-rights policy for the collector's user (policy.json above)
aws iam put-user-policy --user-name kymoscope-history --policy-name kymoscope-history \
--policy-document file://policy.jsonThen alert when days pile up on a collector. history status lists each local day; more than one pending day for long means uploads to your bucket are failing. You can also chart and alert on history_days_pending in the health stream like any other channel.
# watch-history-uploads.ps1: run hourly from Task Scheduler on the collector host
$out = & kymoscope-collector history status --config C:\kymoscope\collector.yaml
$pending = @($out | Select-String -Pattern ': pending').Count
if ($pending -gt 1) {
Write-EventLog -LogName Application -Source "Kymoscope" -EventId 1002 -EntryType Warning `
-Message "Kymoscope: $pending day(s) waiting to upload to your bucket. Check the Storage tab and the collector log."
exit 1
}#!/bin/sh
# /etc/cron.d/kymoscope-uploads: 15 * * * * kymoscope /opt/kymoscope/watch-history-uploads.sh
pending=$(kymoscope-collector history status --config /opt/kymoscope/collector.yaml | grep -c ': pending')
if [ "$pending" -gt 1 ]; then
logger -p user.warning "Kymoscope: $pending day(s) waiting to upload to your bucket"
exit 1
fi