Run your plugins before data reaches storage

Platform-agnostic processing gateway. Point any S3 client at S4 — filter, redact, encrypt, or convert every object with your Wasm plugins.

# Get an API key from the dashboard or use demo mode # Upload — data runs through the plugin pipeline curl -X PUT http://localhost:9000/ingest/data.jsonl \ -H "x-s4-access-key: YOUR_KEY_ID" \ -H "x-s4-secret-key: YOUR_SECRET" \ --data-binary @data.jsonl # Read — filtered data comes back curl http://localhost:9000/ingest/data.jsonl \ -H "x-s4-access-key: YOUR_KEY_ID" \ -H "x-s4-secret-key: YOUR_SECRET"
Everything you need to process data in transit
Multi-cloud storage
S4 Service Storage distributes objects across AWS, R2, B2 using consistent hashing. Dual-write to primary + replica. Cross-cloud resilience by default.
📁
Full S3 API
PUT, GET, DELETE, HEAD, LIST — standard S3 operations run through the plugin pipeline. Drop-in compatible with AWS SDK, boto3, and any S3 tool.
🔑
Simple ACL
S4 API keys (s4_xxx / s4s_xxx) authenticate S3 requests. Create, revoke, set expiry per key. No IAM policy complexity.
🌐
Cloud agnostic
Presigned URL proxy works with S3, R2, B2, MinIO — any S3-compatible storage. BYO backend or use S4's managed buckets.
🧩
Plugin system
Wasm filter plugins: import, enable, disable, reorder at runtime. Ordered pipeline processes data through multiple plugins — ship custom transforms without redeploying.
🛡
Zero trust
IAM Role assumption (Fivetran/Airbyte model). No long-lived credentials stored. Unique External ID per workspace prevents confused deputy attacks.

Get Started

Create an account or sign in
or with email
Create your first API key
S3-compatible credentials. Works with AWS SDK, CLI, or any S3 tool.
Key label
Expiry
Where should S4 write filtered data?
S4 uses the Fivetran/Airbyte model: you create an IAM role (or API token) granting S4 access to your bucket. No long-lived credentials stored.
Step 1: Create an IAM Role for S4
Run this in your AWS account. S4 will assume this role to write filtered data to your bucket.
TRUST_POLICY=$(cat <<'EOF' { "Version": "2012-10-17", "Statement": [{ "Effect": "Allow", "Principal": {"AWS": "arn:aws:iam::S4_ACCOUNT_ID:role/s4-gateway"}, "Action": "sts:AssumeRole", "Condition": {"StringEquals": {"sts:ExternalId": "S4_EXTERNAL_ID"}} }] } EOF ) aws iam create-role --role-name s4-pii-filter \ --assume-role-policy-document "$TRUST_POLICY" aws iam put-role-policy --role-name s4-pii-filter \ --policy-name s4-bucket-access \ --policy-document '{ "Version": "2012-10-17", "Statement": [{ "Effect": "Allow", "Action": ["s3:PutObject","s3:GetObject","s3:ListBucket"], "Resource": ["arn:aws:s3:::YOUR_BUCKET","arn:aws:s3:::YOUR_BUCKET/*"] }] }' echo "Role ARN: $(aws iam get-role --role-name s4-pii-filter --query 'Role.Arn' --output text)"
Step 2: Paste your Role ARN
Copy the Role ARN from the output above and paste it below. Your External ID is auto-generated.
Role ARN
External ID (copy this)
Create an R2 API Token
In the Cloudflare dashboard: R2 → Manage R2 API Tokens → Create API Token → select your bucket → grant Read + Write.
# Get your Account ID from the Cloudflare dashboard ACCOUNT_ID="your-account-id" BUCKET="your-bucket" # The endpoint is always: echo "https://${ACCOUNT_ID}.r2.cloudflarestorage.com"
R2 Endpoint
R2 API Token
Create a B2 Application Key
In the B2 console: App Keys → Add a New Application Key → select your bucket → grant Read + Write.
# Install B2 CLI: brew install backblaze-b2 (or pip install b2) b2 authorize-account b2 get-account-info # Note your accountId. Endpoint is: echo "https://s3.us-west-004.backblazeb2.com"
B2 S3 Endpoint
Key ID
Application Key
Connect to your MinIO instance
S4 connects directly to your MinIO server using access key + secret key.
MinIO Endpoint
Access Key
Secret Key
Connect your tools
Generate a presigned URL for your bucket, then pipe it through S4 for plugin processing.
export S4_KEY=s4_YOUR_KEY_ID export S4_SECRET=s4s_YOUR_SECRET export S4=http://localhost:9000 # Generate a presigned PUT URL using your AWS credentials PRESIGNED=$(aws s3 presign s3://my-bucket/uploads/data.jsonl --expires-in 604800) # Send through S4 — processed by the plugin pipeline before reaching your bucket curl -X PUT "$S4/my-bucket/uploads/data.jsonl" \ -H "x-s4-access-key: $S4_KEY" \ -H "x-s4-secret-key: $S4_SECRET" \ -H "x-s4-backend-url: $PRESIGNED" \ --data-binary @data.jsonl
import boto3, requests # Generate presigned URL with your AWS credentials s3 = boto3.client('s3') presigned = s3.generate_presigned_url( 'put_object', Params={'Bucket': 'my-bucket', 'Key': 'uploads/data.jsonl'}, ExpiresIn=604800 ) # Send through S4 r = requests.put( 'http://localhost:9000/my-bucket/uploads/data.jsonl', headers={ 'x-s4-access-key': 's4_YOUR_KEY_ID', 'x-s4-secret-key': 's4s_YOUR_SECRET', 'x-s4-backend-url': presigned }, data=open('data.jsonl', 'rb') )
use aws_sdk_s3::{Client, presigning::PresigningConfig}; // Generate presigned URL let config = aws_config::load_from_env().await; let s3 = Client::new(&config); let presigned = s3.put_object() .bucket("my-bucket") .key("uploads/data.jsonl") .presigned(PresigningConfig::expires_in( std::time::Duration::from_secs(604800) )).await?; // Send through S4 let client = reqwest::Client::new(); let resp = client.put("http://localhost:9000/my-bucket/uploads/data.jsonl") .header("x-s4-access-key", "s4_YOUR_KEY_ID") .header("x-s4-secret-key", "s4s_YOUR_SECRET") .header("x-s4-backend-url", presigned.uri()) .body(std::fs::read("data.jsonl")?) .send().await?;
// Generate presigned URL import software.amazon.awssdk.services.s3.presigner.S3Presigner; import software.amazon.awssdk.services.s3.model.PutObjectRequest; import java.net.http.*; var presigner = S3Presigner.create(); var req = PutObjectRequest.builder() .bucket("my-bucket").key("uploads/data.jsonl").build(); var presigned = presigner.presignPutObject(p -> p .putObjectRequest(req) .signatureDuration(java.time.Duration.ofDays(7)) ); // Send through S4 var client = HttpClient.newHttpClient(); var httpReq = HttpRequest.newBuilder() .uri(URI.create("http://localhost:9000/my-bucket/uploads/data.jsonl")) .header("x-s4-access-key", "s4_YOUR_KEY_ID") .header("x-s4-secret-key", "s4s_YOUR_SECRET") .header("x-s4-backend-url", presigned.url().toString()) .PUT(HttpRequest.BodyPublishers.ofFile(Path.of("data.jsonl"))) .build(); client.send(httpReq, HttpResponse.BodyHandlers.ofString());
Envelope encryption built in
Python and TypeScript SDKs ship a high-level client. Generate a keypair, hand S4 the public half once, and every PUT is envelope-encrypted server-side (RSA-OAEP + AES-256-GCM). Read back and decrypt with your private key — S4 never sees it.
pip install -e sdks/python requests cryptography from s4_client.highlevel import S4Client client = S4Client("http://localhost:9000", "s4_KEY_ID", "s4s_SECRET") # One-time: generate a keypair and hand S4 the public half. # The envelope-encrypt plugin encrypts on every PUT; only you can decrypt it. private_pem, public_pem = S4Client.generate_keypair() client.attach_public_key(public_pem) # Write — PII envelope-encrypted server-side (RSA-OAEP + AES-256-GCM) client.put_object("my-bucket", "ingest/data.jsonl", open("data.jsonl", "rb").read()) # Read — decrypt the envelopes back to plaintext with your private key raw = client.get_object("my-bucket", "ingest/data.jsonl") plaintext = S4Client.decrypt_payload(raw, private_pem)
npm install sdks/typescript # builds dist/ on install import { S4Client } from "s4-client/highlevel"; const client = new S4Client({ endpoint: "http://localhost:9000", accessKey: "s4_KEY_ID", secretKey: "s4s_SECRET", }); // One-time: hand S4 the public half of a fresh keypair const { privateKeyPem, publicKeyPem } = await S4Client.generateKeypair(); await client.attachPublicKey(publicKeyPem); // Write — PII encrypted server-side (RSA-OAEP + AES-256-GCM) await client.putObject("my-bucket", "ingest/data.jsonl", new TextEncoder().encode(JSON.stringify(records))); // Read — decrypt envelopes client-side const raw = await client.getObject("my-bucket", "ingest/data.jsonl"); const plaintext = new TextDecoder().decode( await S4Client.decryptPayload(raw, privateKeyPem));
Objects in memory
Objects flowing through the gateway. Upload more via S3 API.