Here is the thing that surprises people when they first poke at Squilla internals: the kernel keeps zero bytes on disk. Not one. If you grep the core for code that writes uploaded files to a folder, you will not find it. Every media operation, upload, get, query, delete, is routed to whichever active extension declares provides:["media-provider"]. The kernel only knows about the slot. The thing that knows about S3, or R2, or local disk, lives somewhere else entirely.
That sounds like a small architectural choice. It is not. It is the entire reason you can swap storage backends without touching core, without a restart, and without rewriting any handler that calls core.media.upload.
The slot, not the storage
When the kernel boots, it scans active extensions for capability tags. One of those tags is media-provider. The plugin manager keeps a small atomic pointer to whichever active extension owns that slot at the highest priority. Every CoreAPI call into Media.* resolves the pointer and forwards the call over gRPC.
The bundled media-manager extension fills the slot using local disk. It is the default, and it is fine for most sites. But it is just an extension. If you want S3, you install a cloudfront-media extension at a higher priority and activate it. The pointer flips. The next upload goes to S3. No code change. No restart. No migration of the calling code anywhere in the system.
What the extension actually looks like
The shape is small. The manifest declares the slot and the capability the plugin needs from the kernel:
{
"slug": "cloudfront-media",
"name": "CloudFront Media",
"version": "1.0.0",
"provides": ["media-provider"],
"priority": 100,
"capabilities": ["files:write", "data:write", "data:read"],
"plugin": {
"binary": "bin/cloudfront-media",
"protocol": "grpc"
}
}Two things matter here. The provides array tells the kernel this extension is a candidate for the media-provider slot. The priority field is how the kernel breaks ties when more than one media-provider extension is active. Higher wins. That is the whole election.
The capabilities array is the other direction of the handshake. The plugin is telling the kernel what CoreAPI surface it intends to use. The kernel enforces this at every call. If the manifest does not declare files:write, the plugin cannot call StoreFile on the host, full stop.
The four methods
A media-provider plugin implements four RPCs. The kernel proxies them through the SquillaHost gRPC service to whichever extension currently holds the slot. Here is a sketch of StoreFile for an S3-backed implementation:
func (p *Plugin) StoreFile(ctx context.Context, req *pb.StoreFileRequest) (*pb.StoreFileResponse, error) {
key := buildKey(req.GetFilename(), req.GetMimeType())
_, err := p.s3.PutObject(ctx, &s3.PutObjectInput{
Bucket: aws.String(p.bucket),
Key: aws.String(key),
Body: bytes.NewReader(req.GetBytes()),
ContentType: aws.String(req.GetMimeType()),
})
if err != nil {
return nil, fmt.Errorf("s3 put: %w", err)
}
return &pb.StoreFileResponse{
Id: uuid.NewString(),
Url: p.cdnBase + "/" + key,
MimeType: req.GetMimeType(),
Size: int64(len(req.GetBytes())),
}, nil
}The other three follow the same shape. GetFile returns a URL or a signed URL. QueryFiles reads from whatever index the extension owns (its own table, a tag manifest, the bucket listing if you really want to). DeleteFile drops the object and the index row.
Notice what is not here. There is no logic about thumbnails, no logic about WebP conversion, no logic about which folder local files go into. Those are concerns of specific provider implementations. The kernel does not care.
Hot-swap, atomically
The part I find the most fun is what happens at the moment you flip providers. You go into the admin, deactivate media-manager, activate cloudfront-media. The kernel re-resolves the media-provider slot. It is a single pointer swap, atomic, no locks held on the read path. Every in-flight request finishes against whatever provider it started with. Every new request hits the new one.
There is no restart. There is no draining. There is no special migration step in core. If you want to move existing files from local disk to S3, that is a job the new extension can run as a background task on activation, and that is the new extension's problem to solve, not the kernel's.
Why this matters
This is the magic of the kernel staying generic. The kernel does not know about S3. It does not know about R2. It does not know about local disk. It knows about a media-provider slot, and it knows how to route four method calls to whoever holds it. The thing that knows about S3 is replaceable.
If you take one idea from this post, take that one. The kernel does not need to grow to support new storage backends. The kernel needs to stay small so new storage backends can ship without changing the kernel at all. That is the difference between a CMS you extend and a CMS you fork.