S3 API
g7 buckets speak the S3 API. Point any S3 client at https://g7.tissue.systems with a bucket's credentials and it can list, read, write, and delete objects — the AWS CLI, boto3, rclone, and the S3 SDK for your language all work without a plugin or a compatibility layer.
This is the access path for everything outside a Cell: CI jobs pushing build artifacts, a laptop pulling backups, a media pipeline that already speaks S3. Inside a Cell, use the g7 binding instead — it is faster and needs no credentials.
Endpoint
| Endpoint URL | https://g7.tissue.systems |
| Region | tissue |
| Addressing | Path-style (https://g7.tissue.systems/<bucket>/<key>) |
| Authentication | AWS Signature Version 4 — request headers or presigned query strings |
Every client needs the endpoint URL and the region set explicitly. Most also need path-style addressing; the AWS CLI and boto3 pick it up on their own once endpoint_url is set, and the examples below show the setting for clients that do not.
Get credentials for a bucket
Each bucket has its own access key. A key unlocks exactly one bucket — it cannot see or touch anything else in your account, which makes it safe to hand a key to a single CI job or a single machine.
From the dashboard: open the bucket, then Show credentials.
From the API, with a token carrying the buckets:write scope:
curl -X POST https://api.tissue.systems/v1/buckets/avatars/s3-credentials \
-H "Authorization: Bearer $TISSUE_TOKEN"
{
"bucket": "avatars",
"accessKeyId": "GKa1b2c3d4e5f60718293a4b5c",
"secretAccessKey": "…",
"region": "tissue",
"endpoint": "https://g7.tissue.systems"
}
The call returns the bucket's existing key rather than minting a new one, so it is safe to run twice. Treat the secret like a password: it can write and delete every object in the bucket. Rotating a key is not self-serve yet — contact support if one leaks.
Install the AWS CLI
The examples use AWS CLI v2. Version 1 is unmaintained; if aws --version reports aws-cli/1.x, upgrade.
macOS
curl "https://awscli.amazonaws.com/AWSCLIV2.pkg" -o "AWSCLIV2.pkg"
sudo installer -pkg AWSCLIV2.pkg -target /
Or with Homebrew:
brew install awscli
Linux (x86_64)
curl "https://awscli.amazonaws.com/awscli-exe-linux-x86_64.zip" -o "awscliv2.zip"
unzip awscliv2.zip
sudo ./aws/install
On ARM machines — a Raspberry Pi, an Ampere or Graviton VM — substitute awscli-exe-linux-aarch64.zip.
Windows
msiexec.exe /i https://awscliv2.amazonaws.com/AWSCLIV2.msi
Confirm the install:
aws --version
aws-cli/2.35.7 Python/3.14.6 Darwin/25.5.0 source/arm64
Configure the AWS CLI
Environment variables suit CI jobs and throwaway shells:
export AWS_ACCESS_KEY_ID=GKa1b2c3d4e5f60718293a4b5c
export AWS_SECRET_ACCESS_KEY=…
export AWS_DEFAULT_REGION=tissue
export AWS_ENDPOINT_URL=https://g7.tissue.systems
aws s3 ls s3://avatars/
A named profile suits a workstation you use every day. In ~/.aws/config:
[profile tissue]
region = tissue
endpoint_url = https://g7.tissue.systems
In ~/.aws/credentials:
[tissue]
aws_access_key_id = GKa1b2c3d4e5f60718293a4b5c
aws_secret_access_key = …
Then pass --profile tissue to any command:
aws --profile tissue s3 ls s3://avatars/
A profile per bucket keeps the one-key-one-bucket boundary visible in the command you type. If you would rather not configure anything, --endpoint-url works on every invocation:
aws --endpoint-url https://g7.tissue.systems s3 ls s3://avatars/
AWS CLI examples
The commands below assume AWS_ENDPOINT_URL and credentials are already in the environment.
List objects
aws s3 ls s3://avatars/
aws s3 ls s3://avatars/portraits/ --recursive
Upload and download
aws s3 cp ./photo.jpg s3://avatars/portraits/alice.jpg
aws s3 cp s3://avatars/portraits/alice.jpg ./alice.jpg
Objects larger than 8 MB are split into parts and uploaded in parallel automatically, so a multi-gigabyte file is one cp like any other.
Set the content type so browsers render the object instead of downloading it:
aws s3 cp ./index.html s3://site-assets/index.html \
--content-type text/html \
--cache-control "public, max-age=3600"
Sync a directory, transferring only what changed:
aws s3 sync ./dist s3://site-assets/ --delete
aws s3 sync s3://site-assets/ ./restore
Move, copy, and delete
aws s3 mv s3://avatars/old.jpg s3://avatars/archive/old.jpg
aws s3 rm s3://avatars/portraits/alice.jpg
aws s3 rm s3://avatars/thumbnails/ --recursive
Presign a URL so someone without credentials can fetch one object:
aws s3 presign s3://avatars/portraits/alice.jpg --expires-in 3600
Lower-level calls through s3api when you need the raw response:
aws s3api head-object --bucket avatars --key portraits/alice.jpg
aws s3api list-objects-v2 --bucket avatars --prefix portraits/ --max-keys 100
aws s3api get-object --bucket avatars --key video.mp4 --range 'bytes=0-1048575' first-mb.bin
Python
Install boto3:
pip install boto3
Build a client once and reuse it:
import boto3
from botocore.client import Config
s3 = boto3.client(
"s3",
endpoint_url="https://g7.tissue.systems",
region_name="tissue",
aws_access_key_id="GKa1b2c3d4e5f60718293a4b5c",
aws_secret_access_key="…",
config=Config(s3={"addressing_style": "path"}),
)
addressing_style is explicit here because a client that falls back to virtual-host style would try to resolve avatars.g7.tissue.systems, which does not exist.
Write and read an object
s3.put_object(
Bucket="avatars",
Key="portraits/alice.jpg",
Body=open("photo.jpg", "rb"),
ContentType="image/jpeg",
)
obj = s3.get_object(Bucket="avatars", Key="portraits/alice.jpg")
data = obj["Body"].read()
Upload and download files, with parts and retries handled for you:
s3.upload_file("backup.tar.gz", "backups", "2026-08-09/backup.tar.gz")
s3.download_file("backups", "2026-08-09/backup.tar.gz", "restore.tar.gz")
List every object, paginating past the 1000-key response limit:
paginator = s3.get_paginator("list_objects_v2")
for page in paginator.paginate(Bucket="avatars", Prefix="portraits/"):
for obj in page.get("Contents", []):
print(obj["Key"], obj["Size"])
Presign a URL for a browser upload or a one-off download:
download = s3.generate_presigned_url(
"get_object",
Params={"Bucket": "avatars", "Key": "portraits/alice.jpg"},
ExpiresIn=3600,
)
upload = s3.generate_presigned_url(
"put_object",
Params={"Bucket": "avatars", "Key": "incoming/photo.jpg"},
ExpiresIn=900,
)
A presigned URL carries its own signature, so the holder needs no credentials and no S3 client — a plain PUT or GET is enough. It stops working when it expires.
Copy and delete
s3.copy_object(
Bucket="avatars",
Key="archive/alice.jpg",
CopySource="avatars/portraits/alice.jpg",
)
s3.delete_object(Bucket="avatars", Key="portraits/alice.jpg")
Other S3 clients
rclone — add a remote to ~/.config/rclone/rclone.conf:
[tissue]
type = s3
provider = Other
endpoint = https://g7.tissue.systems
region = tissue
force_path_style = true
access_key_id = GKa1b2c3d4e5f60718293a4b5c
secret_access_key = …
rclone copy ./dist tissue:site-assets/
rclone sync tissue:backups/ ./restore
Any other client needs the same four things: the endpoint URL, region tissue, path-style addressing, and the bucket's key pair.
How this differs from Amazon S3
A key names its bucket. The bucket segment in a request URL is not what selects the bucket — the access key is. Two consequences worth knowing before they surprise you:
aws s3 lswith no bucket returns nothing. There is no ListBuckets operation, because a key can only ever see one bucket.- Typing the wrong bucket name does not fail; the request lands on the key's own bucket. A key cannot reach another bucket by any spelling, so this is a boundary rather than a leak, but a typo in a script is silent.
Path-style addressing only. https://g7.tissue.systems/<bucket>/<key>. Virtual-host style (<bucket>.g7.tissue.systems) does not resolve.
The region is tissue. It is part of the signature, so a client configured for us-east-1 will sign against the wrong scope with some SDKs.
Signatures expire in 15 minutes. A request signed more than 15 minutes away from server time is rejected. If everything returns 403 on a machine that worked yesterday, check its clock first.
100 MiB per request. A single PUT is capped at 100 MiB. Larger objects go up as multipart uploads, where the cap applies per part rather than to the object — the AWS CLI and boto3 do this for you above 8 MB, and there is no ceiling on the finished object.
Bucket-level configuration is not implemented. Versioning, tagging, and object ACLs return NotImplemented. Bucket visibility is a g7 setting rather than an S3 ACL: ribo bucket set <name> --public or --private, described in Managing Buckets.
See also
- Managing Buckets — creating buckets,
ribofile transfer, public access - g7 API Reference — the binding API for use inside a Cell
- g7 Limitations — what g7 does not do