{"record":{"id":"4d98e91e9a2c2da1","repo":"hcengineering/platform","slug":"readonlyerror","errorCode":null,"errorMessage":"ReadonlyError","messagePattern":"ReadonlyError","errorType":"exception","errorClass":"ReadonlyError","httpStatus":null,"severity":"error","filePath":"foundations/server/packages/server-storage/src/readonly.ts","lineNumber":43,"sourceCode":"}\n\nexport class ReadonlyStorageAdapter implements StorageAdapter {\n  constructor (private readonly adapter: StorageAdapter) {}\n\n  async initialize (ctx: MeasureContext, wsIds: WorkspaceIds): Promise<void> {\n    await this.adapter.initialize(ctx, wsIds)\n  }\n\n  async close (): Promise<void> {\n    await this.adapter.close()\n  }\n\n  async exists (ctx: MeasureContext, wsIds: WorkspaceIds): Promise<boolean> {\n    return await this.adapter.exists(ctx, wsIds)\n  }\n\n  async make (ctx: MeasureContext, wsIds: WorkspaceIds): Promise<void> {\n    throw new ReadonlyError()\n  }\n\n  async listBuckets (ctx: MeasureContext): Promise<BucketInfo[]> {\n    return await this.adapter.listBuckets(ctx)\n  }\n\n  async delete (ctx: MeasureContext, wsIds: WorkspaceIds): Promise<void> {\n    throw new ReadonlyError()\n  }\n\n  async remove (ctx: MeasureContext, wsIds: WorkspaceIds, objectNames: string[]): Promise<void> {\n    throw new ReadonlyError()\n  }\n\n  async listStream (ctx: MeasureContext, wsIds: WorkspaceIds): Promise<BlobStorageIterator> {\n    return await this.adapter.listStream(ctx, wsIds)\n  }\n","sourceCodeStart":25,"sourceCodeEnd":61,"githubUrl":"https://github.com/hcengineering/platform/blob/63e28dc96483967b2fc21c881b3f1023c1de7718/foundations/server/packages/server-storage/src/readonly.ts#L25-L61","documentation":"ReadonlyStorageAdapter wraps a real StorageAdapter and deliberately throws ReadonlyError('Readonly mode') from mutating methods. make() (which creates/initializes a workspace bucket/prefix) is one of them. The wrapper is enabled when a storage config has readonly=true (see createStorageFromConfig). It is an intentional policy rejection, not a bug — all writes are blocked while reads delegate to the underlying adapter.","triggerScenarios":"Any code path calling make(ctx, wsIds) on a ReadonlyStorageAdapter — typically server startup or first access to a workspace that doesn't exist yet in a storage configured with readonly=true in STORAGE_CONFIG (e.g. appending '|readonly=true' or setting the readonly query param on the URI), or programmatic use of ReadonlyStorageAdapter as a wrapper.","commonSituations":"Pointing a full (writable) service at a read-only replica/backup bucket by mistake; migrating between storages where the new primary is mistakenly marked readonly; copy-pasting a STORAGE_CONFIG example that included readonly=true; disaster-recovery runbook executed with the read-only flag still set.","solutions":["Remove the readonly flag from the storage URI in STORAGE_CONFIG (e.g. drop &readonly=true) for the storage that must accept writes","If the bucket genuinely must stay read-only, create the workspace in a different writable storage and mount the readonly one only as a fallback/read source","Verify which adapter throws by logging config.kind/name before createStorageFromConfig","Check that a recent deployment didn't flip readonly=true unintentionally"],"exampleFix":"// before\nSTORAGE_CONFIG=minio|minio:9000?accessKey=minio&secretKey=minio&readonly=true\n// after\nSTORAGE_CONFIG=minio|minio:9000?accessKey=minio&secretKey=minio","handlingStrategy":"validation","validationCode":"// before calling make(), ensure the target storage is writable\nconst cfg = storageConfig.storages.find(s => s.name === targetName)\nif (cfg?.readonly === 'true') {\n  throw new Error(`storage ${targetName} is readonly; make() would fail`)\n}","typeGuard":"function isReadonlyStorageAdapter (a: StorageAdapter): a is ReadonlyStorageAdapter {\n  return a instanceof ReadonlyStorageAdapter\n}","tryCatchPattern":"try {\n  await storage.make(ctx, wsIds)\n} catch (err) {\n  if (err.name === 'ReadonlyError') {\n    // storage is intentionally immutable; create workspace elsewhere or skip\n    return skipWithWarning(wsIds)\n  }\n  throw err\n}","preventionTips":["Keep readonly=true only on fallback/read replicas in STORAGE_CONFIG","Check the readonly flag at service startup and fail fast if the primary is readonly","Document which environments run against read-only storage","When copying STORAGE_CONFIG between envs, re-check the readonly params"],"tags":["storage","readonly","configuration","write-blocked"],"backgroundTag":"readonly-storage","analyzedSha":"63e28dc96483967b2fc21c881b3f1023c1de7718","analyzedAt":"2026-08-29T15:21:27.377Z","schemaVersion":2},"datasetVersion":"2026-08-29T17:17:51.833Z"}