Platform-agnostic processing gateway. Point any S3 client at S4 — filter, redact, encrypt, or convert every object with your Wasm plugins.
Quick Start
# 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"
Features
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
Get started
Create your first API key
S3-compatible credentials. Works with AWS SDK, CLI, or any S3 tool.
Key label
Expiry
Backend storage
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.
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.
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());
Client SDKs
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));
Storage
Objects in memory
Objects flowing through the gateway. Upload more via S3 API.