Set up a bucket
What the bucket must support, and three ways to get one: AWS S3 with the Terraform module, Tigris, and a local SeaweedFS container.
A notebook is one bucket plus one prefix. The prefix (default
slivingdoc) is the path inside the bucket where the notebook’s objects
live, and slivingdoc creates it with the first commit. The bucket itself
must already exist: slivingdoc never creates or configures one.
my-notes in these examples is a bucket name. On AWS that name is the
first label of the host — my-notes.s3.eu-north-1.amazonaws.com — while
a custom endpoint takes it as a path segment,
https://t3.storage.dev/my-notes/…. The region or the endpoint decides
which store answers; the name only decides which container in it.
What the bucket must support
Before the first MCP call, the server runs a disposable compatibility
probe below the configured prefix. The probe proves that the store
enforces If-None-Match: * creation, If-Match replacement, and
read-after-write behaviour — the three conditional-write guarantees the
publication protocol depends on. A store that fails the probe is refused
at startup with the INCOMPATIBLE_STORE category, and when the failure
is operational rather than a missing capability the diagnostic names the
underlying reason, with the probe key and any secret redacted. Bucket
versioning is not required. See
S3 requirements for the exact permission set.
Three settings differ per store:
| Setting | Flag | Note |
|---|---|---|
| Endpoint | --endpoint |
Empty for AWS. An absolute http or https URL for anything else. |
| Region | --region |
us-east-1 unless you set it. |
| Path style | --path-style |
Automatic for a custom endpoint; this flag extends it to AWS. |
A custom endpoint always uses path-style addressing, so --path-style is
only needed to force path style on the default AWS endpoint. A
--endpoint URL that carries user information is refused, so a secret
can never echo into a diagnostic.
AWS S3, with the Terraform module
The repository ships a reusable module that provisions the private bucket and the least-privilege IAM user whose access keys the server uses:
module "notebook" {
source = "github.com/baalimago/slivingdoc//terraform"
bucket_name = "my-notes"
iam_user_name = "my-notes-mcp"
tags = {
purpose = "slivingdoc-notes"
}
}
output "access_key_id" {
value = module.notebook.access_key_id
sensitive = true
}
output "secret_access_key" {
value = module.notebook.secret_access_key
sensitive = true
}
Configure the aws provider in your own root module; the module does not
set a region. The //terraform suffix selects the subdirectory of the
repository, and no ?ref means the default branch — pin a release with
?ref=vX.Y.Z.
The module creates the bucket with versioning enabled,
BucketOwnerEnforced ownership, SSE-S3 default encryption with the
bucket key enabled, SSE-C uploads blocked, and all four public access
blocks on. A bucket policy denies every S3 action to every principal
except the created IAM user and the AWS account root. The root exemption
is deliberate: it is the recovery path, and the only way
terraform destroy can complete. The IAM user carries one inline policy
scoped to this bucket alone.
Its inputs are bucket_name (required), iam_user_name (defaults to the
bucket name), tags, and force_destroy, which is needed to delete a
bucket that still holds notebook state. Read the keys from the sensitive
outputs:
terraform output -raw access_key_id
terraform output -raw secret_access_key
Then point the server at it. The endpoint stays empty, and the region is the bucket’s region:
export SLIVINGDOC_BUCKET=my-notes
slivingdoc serve --region eu-north-1
The repository’s
terraform/README.md
documents the module, and
examples/terraform/
is a worked root configuration.
Tigris
Tigris is an S3-compatible store with one global endpoint, and it supports the conditional writes the startup probe requires. Create a bucket and an access key pair in its console, then set the endpoint and the region:
export SLIVINGDOC_BUCKET=my-notes
export AWS_ACCESS_KEY_ID=<your-access-key-id>
export AWS_SECRET_ACCESS_KEY=<your-secret-access-key>
slivingdoc serve \
--endpoint https://t3.storage.dev \
--region auto
Path-style addressing is automatic for a custom endpoint, so no further flag is needed.
Local SeaweedFS, for evaluation
One pinned container gives you an S3-compatible store with no account at
all. The compose file is
examples/seaweedfs/
in the repository. From that directory:
docker compose up -d
The S3 gateway listens on http://localhost:8333. The credentials are
fixed in the compose file — slivingdoc and slivingdoc-local — and
exist only inside that container. There is no bucket step: my-notes is
created on the first write, which the startup probe performs, so the
bucket exists by the time the server accepts MCP calls.
export SLIVINGDOC_BUCKET=my-notes
export AWS_ACCESS_KEY_ID=slivingdoc
export AWS_SECRET_ACCESS_KEY=slivingdoc-local
slivingdoc serve \
--endpoint http://localhost:8333 \
--path-style \
--workspace-root /tmp/notes \
--private-root /tmp/slivingdoc-private
The two root flags are optional — without them the server takes its own temporary notebook directory and removes it at shutdown — but naming them lets you open the files yourself while following along. See Share a directory with humans.
Note: The container logs a
no signing key found for STS serviceline on startup. It is not fatal, and it is safe to ignore for basic credentials.
Stop it when you are done:
docker compose down # stop the container, keep the notes volume
docker compose down -v # also delete the notes volume
Warning: This container is for evaluation on one machine. Its credentials are public, in a file in a public repository.
Next
- Quickstart — the first pull and commit against the bucket you just made.
- S3 requirements — permissions, the probe, and what the store must guarantee.
- Connect an MCP host — how the credentials reach the server.