Skip to main content

Storage Backends

Starfish's ObjectStore interface abstracts over any key/value store. The library ships two built-in implementations: an in-process filesystem store for development and a self-hosted node, and an S3-compatible store for production deployments.

FilesystemObjectStore

Stores every document as a file on disk. Suitable for single-node deployments, local development, and the Ansible-based server role.

Import path: @drakkar.software/starfish-server/node

import { FilesystemObjectStore } from "@drakkar.software/starfish-server/node"

const store = new FilesystemObjectStore({ baseDir: "/var/lib/starfish/data" })
OptionTypeDescription
baseDirstringRoot directory where objects are stored. Created automatically if absent.

S3ObjectStore

Stores documents in any S3-compatible object store (AWS S3, MinIO, Cloudflare R2, Tigris, etc.). Suitable for horizontally-scaled or serverless deployments.

Import path: @drakkar.software/starfish-server/s3

Peer dependency: @aws-sdk/client-s3 >= 3.0.0 must be installed separately.

npm install @aws-sdk/client-s3
import { S3ObjectStore } from "@drakkar.software/starfish-server/s3"

const store = new S3ObjectStore({
accessKeyId: process.env.S3_ACCESS_KEY_ID!,
secretAccessKey: process.env.S3_SECRET_ACCESS_KEY!,
endpoint: "https://s3.amazonaws.com",
bucket: "my-starfish-bucket",
region: "us-east-1",
})

Options

OptionTypeDefaultDescription
accessKeyIdstringAWS / S3-compatible access key ID
secretAccessKeystringAWS / S3-compatible secret access key
endpointstringBase URL of the S3 service
bucketstringBucket name
regionstring"us-east-1"AWS region
forcePathStylebooleantrueUse path-style addressing (required for MinIO and most non-AWS services)

Cleanup

Call store.destroy() on shutdown to release the underlying HTTP connections:

process.on("SIGTERM", () => {
store.destroy()
process.exit(0)
})

MinIO example

const store = new S3ObjectStore({
accessKeyId: "minioadmin",
secretAccessKey: "minioadmin",
endpoint: "http://localhost:9000",
bucket: "starfish",
region: "us-east-1", // MinIO ignores this but it must be set
forcePathStyle: true, // required for MinIO
})

Cloudflare R2 example

const store = new S3ObjectStore({
accessKeyId: process.env.R2_ACCESS_KEY_ID!,
secretAccessKey: process.env.R2_SECRET_ACCESS_KEY!,
endpoint: `https://${process.env.CF_ACCOUNT_ID}.r2.cloudflarestorage.com`,
bucket: "starfish",
forcePathStyle: false, // R2 uses virtual-hosted-style
})

MemoryObjectStore

In-process store backed by a JavaScript Map. For unit tests only — data is lost on restart.

import { MemoryObjectStore } from "@drakkar.software/starfish-server"

const store = new MemoryObjectStore(new Map())

Bring your own store

Implement the ObjectStore interface to plug in any backend:

import type { ObjectStore } from "@drakkar.software/starfish-server"

class MyStore implements ObjectStore {
async getString(key: string) { ... }
async put(key: string, body: string) { ... }
async listKeys(prefix: string) { ... }
async delete(key: string) { ... }
async deleteMany(keys: string[]) { ... }
}

Request metadata via StoreContext

Every store method accepts an optional trailing context?: StoreContext argument. When a request comes in through a route handler the library fills this with structured metadata about the request:

interface StoreContext {
collection: string // collection name from config (e.g. "profile")
namespace?: string // set when the route lives under a namespace mount
params: Record<string, string> // resolved path params (e.g. { identity: "alice" })
identity: string | null // authenticated caller, or null for public routes
roles: readonly string[] // resolved roles for this caller
action: "pull" | "push" | "list" | "delete"
}

CustomObjectStore — receiving context in callbacks

Callbacks that accept an extra trailing argument automatically receive the context. Callbacks written with the old single-argument signature continue to work unchanged — no migration required.

import { CustomObjectStore } from "@drakkar.software/starfish-server"

const store = new CustomObjectStore({
// Old-style — still works, ctx is silently ignored
onGet: (key) => myBackend.get(key),

// New-style — receives full request context
onPut: (key, body, ctx) => {
console.log(`${ctx?.identity} pushed to ${ctx?.collection}`)
return myBackend.set(key, body)
},
})

System-internal calls (replica sync, config loading, enrichers) pass undefined — your callback should treat a missing context as "no request context available".