amruthpillai/reactive-resume · error · Error

Private storage writes are not supported by the local filesy

Error message

Private storage writes are not supported by the local filesystem backend. Configure S3 to store private attachments.

What it means

LocalStorageService.write throws a plain Error when called with private:true. The local filesystem backend has no access-control boundary, so private attachments (e.g. application documents) cannot be stored safely. The message tells operators to configure S3. Unlike the oRPC errors above, this is a generic Error — it will surface as HTTP 500 unless wrapped.

Source

Thrown at packages/api/src/features/storage/service.ts:142

		const fullPath = this.resolvePath(prefix);

		try {
			const files = await fs.readdir(fullPath, { recursive: true });

			return files.map((file) => join(prefix, file));
		} catch (error: unknown) {
			// If directory doesn't exist, return empty array
			if (error && typeof error === "object" && "code" in error && error.code === "ENOENT") {
				return [];
			}

			throw error;
		}
	}

	async write({ key, data, private: isPrivate }: StorageWriteInput): Promise<void> {
		if (isPrivate) {
			throw new Error(
				"Private storage writes are not supported by the local filesystem backend. Configure S3 to store private attachments.",
			);
		}

		const fullPath = this.resolvePath(key);

		await fs.mkdir(dirname(fullPath), { recursive: true });
		await fs.writeFile(fullPath, data);
	}

	async read(key: string): Promise<StorageReadResult | null> {
		const fullPath = this.resolvePath(key);
		try {
			const [arrayBuffer, stats] = await Promise.all([fs.readFile(fullPath), fs.stat(fullPath)]);

			return {
				data: arrayBuffer,
				size: stats.size,

View on GitHub (pinned to 3a5b12e2a4)

Solutions

  1. Configure S3-compatible storage: set S3_ACCESS_KEY_ID, S3_SECRET_ACCESS_KEY, S3_BUCKET (and optionally S3_ENDPOINT/REGION/FORCE_PATH_STYLE), or start the seaweedfs compose service.
  2. If S3 is unavailable in your environment, disable the feature that requests private writes rather than allowing the write to be attempted.
  3. Gate the private-write code path on a capability check (e.g. storage.getCapabilities().supportsPrivate) and degrade gracefully.
  4. Confirm getStorageService() actually selects S3 after the env change by hitting the storage healthcheck endpoint.

Example fix

// before: unconditional private write
await storage.write({ key, data, contentType: 'application/pdf', private: true });
// after: guard on backend capability
if (!storage.supportsPrivateWrites()) {
  return c.json({ error: 'Private attachments require S3 storage.' }, 501);
}
await storage.write({ key, data, contentType: 'application/pdf', private: true });
Defensive patterns

Strategy: validation

Validate before calling

function supportsPrivateWrites(): boolean {
  return Boolean(env.S3_ACCESS_KEY_ID && env.S3_SECRET_ACCESS_KEY && env.S3_BUCKET);
}
if (isPrivate && !supportsPrivateWrites()) throw new ConfigError('Configure S3 for private attachments.');

Type guard

function isPrivateWriteInput(i: StorageWriteInput): i is StorageWriteInput & { private: true } {
  return i.private === true;
}

Try / catch

try { await storage.write({ key, data, contentType, private: true }); }
catch (e) {
  if (/local filesystem backend/.test(String((e as Error).message))) { /* degrade: disable feature or use S3 */ }
  throw e;
}

Prevention

When it happens

Trigger: Any code path that requests a private write (StorageWriteInput.private = true) while S3_ACCESS_KEY_ID / S3_SECRET_ACCESS_KEY / S3_BUCKET are unset, so getStorageService() returned the LocalStorageService. Most commonly: uploading an application/cover-letter attachment marked private.

Common situations: Local/dev deployments that omitted S3/SeaweedFS config but enabled features requiring private attachments; a feature flag turning on private uploads without provisioning S3; CI without SeaweedFS running a flow that uploads private docs.

Related errors


AI-assisted analysis of amruthpillai/reactive-resume@3a5b12e2a4 (2026-08-12). Data as JSON: /api/errors/499d065a2cb9f10f. Report an issue: GitHub.