@classytic/arc-media
Two-phase direct-to-storage uploads, content-hash dedup, and media registry —
@classytic/media-kitas one composable Arc resource.
The module every host otherwise hand-rolls around a blob store. Bytes never
stream through your API server: start-write mints a presigned target (or a
multipart/resumable session for big files), the client uploads straight to
storage, and complete-write registers the asset storage-verified (the
kernel re-checks exists/stat/MIME/tenant-bound key — client claims are never
trusted). A client that sends a content hash (@classytic/media-transform's
sha256Hex) gets the dedup handshake: on a tenant-scoped hash hit, no
upload happens at all.
Direct wire create/update are disabled — the verified verbs are the only
way bytes enter. DELETE respects the engine's soft-delete + storage cleanup.
Install
npm install @classytic/arc-media @classytic/media-kit @classytic/arc @classytic/mongokit mongoose
Peers: @classytic/arc >=2.21.0 · @classytic/media-kit >=3.6.0 ·
@classytic/mongokit >=3.21.0 · mongoose >=9.4.1.
Usage
import { createApp } from '@classytic/arc/factory';
import { schedulesPlugin } from '@classytic/arc/plugins';
import { requireAuth, requireRoles } from '@classytic/arc/permissions';
import { createMedia } from '@classytic/media-kit';
import { createS3Provider } from '@classytic/media-kit/providers/s3';
import { createMediaModule, mediaMaintenanceSchedules } from '@classytic/arc-media';
const engine = await createMedia({ connection, driver: createS3Provider({ /* ... */ }) });
const app = await createApp({
modules: [
createMediaModule({
engine,
permissions: {
view: requireAuth(),
upload: requireAuth(),
manage: requireRoles(['admin']),
},
}),
],
plugins: async (f) => {
await f.register(schedulesPlugin, {
// lock: createMongoLockAdapter({ connection }) — for multi-replica
schedules: mediaMaintenanceSchedules(engine),
});
},
});
Surface
| Route | What |
|---|---|
POST /media/start-write |
Dedup short-circuit (sha256) → else presigned PUT → else multipart/resumable session (multipart/partCount/size ≥ threshold) |
POST /media/complete-write |
Confirm a presigned upload; stores client display hints (thumbhash, dominantColor, width, height) |
POST /media/sign-parts |
On-demand part URLs for an open multipart session |
POST /media/complete-multipart |
Assemble parts, register the asset |
POST /media/:id/action { action: 'signed-url' } |
Time-boxed signed read URL (needs engine signing config) |
GET /media, GET /media/:id |
Adapter reads (QueryParser filters: status, mimeType, folder, visibility, hash, filename, tags) |
DELETE /media/:id |
Kernel delete (soft per engine config, storage cleanup) |
mediaMaintenanceSchedules(engine) returns ScheduleDefinition[] for arc's
schedulesPlugin — stale-pending (hourly), soft-delete purge (daily), and
expiry purge (hourly) sweeps; pass a lock for multi-replica leader safety.
Abandoned uploads that never reached complete-write leave unregistered
storage keys — keep bucket lifecycle rules on as belt-and-braces.
The engine is exported at fastify.arc.modules.media. Host seams (extra
routes/actions, cache, field rules) inject via seams: — arc 2.21
mergeResourceConfig semantics.
License
MIT Classytic