# Effect Platform Abstraction

> Use Effect platform abstractions for cross-platform file I/O, process spawning, HTTP clients, cryptography, and terminal I/O. Apply when writing filesystem/process/HTTP/crypto/console code that must stay portable across Node.js, Bun, and browser adapters.

- Skill: `mpsuesser/effect-platform-abstraction` (Agent Skill)
- Install (CLI): `npx skillmds@latest add mpsuesser/effect-platform-abstraction`
- Raw SKILL.md: https://api.skillmd.com/api/skills/mpsuesser/effect-platform-abstraction/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Coding & Dev Tools
- Author: mpsuesser (https://skillmd.com/u/mpsuesser)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/mpsuesser/effect-platform-abstraction

---


# Platform Abstraction with Effect

## Effect Source Reference

The Effect v4 source is available at `~/.local/share/opencode/repos/github.com/Effect-TS/effect@main/`.
Browse and read files there directly to look up APIs, types, and implementations.

Reference this for:

- FileSystem source: `packages/effect/src/FileSystem.ts`
- Path source: `packages/effect/src/Path.ts`
- Crypto source: `packages/effect/src/Crypto.ts`
- Socket source: `packages/effect/src/unstable/socket/`
- Platform layers: `packages/platform/node/`, `packages/platform/bun/`, and `packages/platform/browser/`
- Migration guide: `MIGRATION.md`
- Effect source: `packages/effect/src/`

## Overview

Effect provides platform-independent abstractions with Node.js and Bun adapters, plus browser adapters for supported services such as HTTP and Crypto. Instead of using runtime-specific APIs directly, you write code once using Effect Platform services and provide the appropriate layer at the edge.

**When to use this skill:**

- Writing file system operations
- Spawning child processes or executing commands
- Making HTTP requests
- Generating cryptographic random bytes, UUIDs, or digests
- Reading CLI arguments or environment variables
- Performing console/terminal I/O
- Working with paths across different operating systems
- Building cross-platform applications or libraries

## Why Effect Platform?

### 1. Cross-Platform Compatibility

Write once, run anywhere:

```typescript
import { Effect, FileSystem } from 'effect';

// Works on Node.js and Bun
const readConfig = Effect.gen(function* () {
	const fs = yield* FileSystem.FileSystem;
	return yield* fs.readFileString('config.json');
});
```

### 2. Type-Safe Error Handling

All operations track errors in the Effect type signature:

```typescript
import { Effect, FileSystem } from 'effect';

// Effect<string, PlatformError, FileSystem>
//        ↓           ↓              ↓
//                                   Required service
//                     Typed error channel
//          Success value
```

### 3. Resource Safety

Automatic cleanup with `Scope`:

```typescript
import { Effect, FileSystem } from 'effect';

const program = Effect.gen(function* () {
	const fs = yield* FileSystem.FileSystem;
	// Read entire file into memory
	return yield* fs.readFile('data.txt');
});
```

### 4. Testability

Easy to mock and stub services:

```typescript
import { Effect, FileSystem, Layer } from 'effect';

declare const myProgram: Effect.Effect<void, never, FileSystem.FileSystem>;

const TestFileSystem = Layer.succeed(
	FileSystem.FileSystem,
	FileSystem.make({
		readFile: () => Effect.succeed(new Uint8Array())
	})
);

const test = myProgram.pipe(Effect.provide(TestFileSystem));
```

### 5. Composability

Integrates naturally with Effect's service system:

```typescript
import { Effect, FileSystem, Layer, Path, Context } from 'effect';

interface ConfigService {
	readonly load: (name: string) => Effect.Effect<string>;
}

const ConfigService = Context.Service<ConfigService>('ConfigService');

const ConfigServiceLive = Layer.effect(
	ConfigService,
	Effect.gen(function* () {
		const fs = yield* FileSystem.FileSystem;
		const path = yield* Path.Path;

		return {
			load: (name: string) =>
				Effect.gen(function* () {
					const configPath = path.join('configs', name);
					return yield* fs.readFileString(configPath);
				})
		};
	})
);
```

## Core Platform Modules

### FileSystem - File Operations

The `FileSystem` service provides comprehensive file and directory operations.

**Anti-Pattern - Direct Node/Bun APIs:**

```typescript
// ❌ WRONG - Platform-specific, not testable
import * as fs from 'fs';
import { readFile } from 'fs/promises';

const content = fs.readFileSync('file.txt', 'utf-8');
const asyncContent = await readFile('file.txt', 'utf-8');

// ❌ WRONG - Bun-specific
declare const Bun: {
	file: (path: string) => { text: () => Promise<string> };
};

const file = Bun.file('file.txt');
const content = await file.text();
```

**Correct Pattern - FileSystem Service:**

```typescript
import { Effect, FileSystem } from 'effect';

// ✅ CORRECT - Cross-platform, type-safe, testable
const readFile = (path: string) =>
	Effect.gen(function* () {
		const fs = yield* FileSystem.FileSystem;
		return yield* fs.readFileString(path);
	});

// Effect<string, PlatformError, FileSystem>
```

**Common Operations:**

```typescript
import { Effect, FileSystem } from 'effect';

const fileOperations = Effect.gen(function* () {
	const fs = yield* FileSystem.FileSystem;

	// Read files
	const text = yield* fs.readFileString('data.txt');
	const bytes = yield* fs.readFile('binary.dat');

	// Write files
	yield* fs.writeFileString('output.txt', 'Hello World');

	// Directory operations
	yield* fs.makeDirectory('new-dir', { recursive: true });
	const files = yield* fs.readDirectory('src');

	// File metadata
	const stats = yield* fs.stat('file.txt');
	const exists = yield* fs.exists('config.json');

	// Copy and move
	yield* fs.copy('source.txt', 'dest.txt');
	yield* fs.rename('old.txt', 'new.txt');

	// Remove files/directories
	yield* fs.remove('temp-file.txt');
	yield* fs.remove('temp-dir', { recursive: true });

	// Temporary files (auto-cleanup with Scope)
	const tempFile = yield* fs.makeTempFileScoped();
	yield* fs.writeFileString(tempFile, 'temporary data');
	// File automatically deleted when scope closes
});
```

`fs.watch(directory)` reports direct-child changes by default; pass `{ recursive: true }` to include nested subdirectories. For open handles, `file.seek(offset, 'start' | 'current')` returns the new offset as a branded `FileSystem.Size`. The old `FileSystem.File.Descriptor` type and `file.descriptor` property were removed in beta.103; use scoped `File` methods instead.

**Streaming Files:**

```typescript
import { Effect, FileSystem, Stream } from 'effect';

declare const processChunk: (chunk: Uint8Array) => Effect.Effect<void>;

// Stream large files efficiently
const processLargeFile = Effect.gen(function* () {
	const fs = yield* FileSystem.FileSystem;

	// Read as stream
	const stream = fs.stream('large-file.txt', { chunkSize: 64 * 1024 });

	// Process stream
	yield* stream.pipe(
		Stream.mapEffect((chunk) => processChunk(chunk)),
		Stream.run(fs.sink('output.txt'))
	);
});
```

### Path - Path Manipulation

The `Path` service provides cross-platform path operations.

**Anti-Pattern - Manual String Manipulation:**

```typescript
// ❌ WRONG - Breaks on Windows, brittle
import path from 'path';

declare const process: { cwd: () => string };
declare const filename: string;

const configPath = './config/' + filename + '.json';
const absPath = process.cwd() + '/' + configPath;

// ❌ WRONG - Node-specific
const joined = path.join('src', 'components', 'Button.tsx');
```

**Correct Pattern - Path Service:**

```typescript
import { Effect, Path } from 'effect';

// ✅ CORRECT - Cross-platform path handling
const buildPath = (filename: string) =>
	Effect.gen(function* () {
		const path = yield* Path.Path;

		// Join paths correctly for any OS
		const configPath = path.join('config', `${filename}.json`);

		// Resolve to absolute path
		const absolutePath = path.resolve(configPath);

		// Extract path components
		const dir = path.dirname(absolutePath);
		const base = path.basename(absolutePath);
		const ext = path.extname(absolutePath);

		// Parse path into components
		const parsed = path.parse(absolutePath);
		// { root, dir, base, ext, name }

		return absolutePath;
	});
```

**Path Operations:**

```typescript
import { Effect, Path } from 'effect';

const pathOps = Effect.gen(function* () {
	const path = yield* Path.Path;

	// Platform-specific separator ("/" or "\")
	const sep = path.sep;

	// Join multiple segments
	const filePath = path.join('src', 'lib', 'utils.ts');

	// Resolve relative paths
	const absolute = path.resolve('..', 'config', 'app.json');

	// Get relative path between two paths
	const rel = path.relative('/app/src', '/app/dist');

	// Check if path is absolute
	const isAbs = path.isAbsolute('/usr/local');

	// Normalize path (remove "..", ".", etc.)
	const normalized = path.normalize('src/../lib/./utils.ts');

	// Work with file URLs
	const url = yield* path.toFileUrl('/path/to/file');
	const fromUrl = yield* path.fromFileUrl(new URL('file:///path/to/file'));
});
```

In Effect v4, `Migrator.fromFileSystem` requires both `FileSystem.FileSystem` and `Path.Path` because migration module paths are converted to file URLs before dynamic import. Aggregate Node/Bun service layers satisfy both requirements. When providing services individually on Windows, use the host-aware path layer rather than core `Path.layer`, which has POSIX semantics.

### ChildProcess - Process Execution

The `ChildProcess` and `ChildProcessSpawner` services enable safe process spawning.

**Anti-Pattern - Direct child_process:**

```typescript
// ❌ WRONG - Node-specific, no resource safety
import { spawn, exec } from 'child_process';
import { promisify } from 'util';

const execAsync = promisify(exec);
const { stdout } = await execAsync('ls -la');

// ❌ WRONG - Bun-specific
declare const Bun: {
	spawn: (cmd: string[]) => { stdout: ReadableStream };
};
declare const Response: {
	new (stream: ReadableStream): { text: () => Promise<string> };
};

const proc = Bun.spawn(['ls', '-la']);
const output = await new Response(proc.stdout).text();
```

**Correct Pattern - ChildProcess + ChildProcessSpawner:**

```typescript
import { ChildProcess, ChildProcessSpawner } from 'effect/unstable/process';
import { Effect, Stream } from 'effect';

// ✅ CORRECT - Cross-platform command execution
const runCommand = Effect.gen(function* () {
	const spawner = yield* ChildProcessSpawner.ChildProcessSpawner;

	// Create command and collect output as string
	const output = yield* spawner.string(ChildProcess.make('ls', ['-la']));

	return output;
});
```

**Advanced ChildProcess Usage:**

```typescript
import { ChildProcess, ChildProcessSpawner } from 'effect/unstable/process';
import { Console, Effect, Stream } from 'effect';

const commandExamples = Effect.gen(function* () {
	const spawner = yield* ChildProcessSpawner.ChildProcessSpawner;

	// Collect output as string
	const stdout = yield* spawner.string(ChildProcess.make('git', ['status']));

	// Collect output as lines
	const lines = yield* spawner.lines(
		ChildProcess.make('git', ['log', '--pretty=format:%s', '-n', '10'])
	);

	// Pipe commands together
	const pipeline = ChildProcess.make('cat', ['file.txt']).pipe(
		ChildProcess.pipeTo(ChildProcess.make('grep', ['error'])),
		ChildProcess.pipeTo(ChildProcess.make('wc', ['-l']))
	);
	const pipelineOutput = yield* spawner.string(pipeline);

	// Set environment variables
	const withEnv = ChildProcess.make('node', ['script.js'], {
		env: { NODE_ENV: 'production', API_KEY: 'secret' },
		extendEnv: true
	});

	// Spawn process and stream output
	const handle = yield* spawner.spawn(
		ChildProcess.make('npm', ['run', 'build'])
	);
	yield* handle.all.pipe(
		Stream.decodeText(),
		Stream.splitLines,
		Stream.runForEach((line) => Console.log(`[build] ${line}`))
	);
	const exitCode = yield* handle.exitCode;

	return exitCode;
});
```

### Terminal - Terminal I/O

The `Terminal` service provides interactive terminal capabilities.

**Anti-Pattern - Direct Console:**

```typescript
// ❌ WRONG - Uses global console, not testable
declare const console: {
	log: (msg: string) => void;
	error: (msg: string) => void;
};
declare const process: {
	stdout: { write: (msg: string) => void };
};
declare const prompt: (msg: string) => string | null;

console.log('Hello World');
console.error('Error occurred');
process.stdout.write('Output\n');

// ❌ WRONG - Not trackable in Effect type
const input = prompt('Enter name:');
```

**Correct Pattern - Terminal Service:**

```typescript
import { Effect, Terminal } from 'effect';

// ✅ CORRECT - Trackable, testable terminal I/O
const interactiveProgram = Effect.gen(function* () {
	const terminal = yield* Terminal.Terminal;

	// Display output
	yield* terminal.display('Hello World\n');

	// Read user input
	const name = yield* terminal.readLine;
	yield* terminal.display(`Welcome, ${name}!\n`);

	// Get terminal dimensions
	const cols = yield* terminal.columns;
	const rows = yield* terminal.rows;
	yield* terminal.display(`Terminal size: ${cols}x${rows}\n`);
});
```

**For Simple Logging - Use Console or Effect.log:**

```typescript
import { Console, Effect } from 'effect';

// ✅ CORRECT - Console service (from effect)
const logging = Effect.gen(function* () {
	yield* Console.log('Info message');
	yield* Console.error('Error message');
	yield* Console.warn('Warning');
	yield* Console.debug('Debug info');
});

// ✅ CORRECT - Effect.log with structured logging
const structuredLog = Effect.gen(function* () {
	yield* Effect.log('Operation started');
	yield* Effect.logDebug('Debug details');
	yield* Effect.logError('Error occurred');

	// With annotations
	yield* Effect.log('User action').pipe(
		Effect.annotateLogs('userId', '123'),
		Effect.annotateLogs('action', 'login')
	);
});
```

### Crypto - Cryptographic Randomness, UUIDs, and Digests

The `Crypto.Crypto` service provides platform-backed cryptographic random bytes, UUIDv4/v7 generation, and message digests. Prefer it over `globalThis.crypto`, `crypto.randomUUID()`, or ad-hoc randomness when code should stay platform-abstract and testable.

```typescript
import { Crypto, Effect } from 'effect';

const cryptoProgram = Effect.gen(function* () {
	const crypto = yield* Crypto.Crypto;

	const bytes = yield* crypto.randomBytes(32);
	const uuidV4 = yield* crypto.randomUUIDv4;
	const uuidV7 = yield* crypto.randomUUIDv7;
	const digest = yield* crypto.digest('SHA-256', bytes);

	return { bytes, uuidV4, uuidV7, digest };
});
```

`NodeServices.layer` and `BunServices.layer` include `Crypto.Crypto`. Browser applications can provide `BrowserCrypto.layer` from `@effect/platform-browser`.

### HttpClient - HTTP Requests

The `HttpClient` service provides type-safe HTTP operations. Runtime application and provider code must use it rather than raw `fetch`. Raw fetch is reserved for an explicitly named low-level platform adapter whose documentation justifies why an Effect transport cannot be used; that adapter must own interruption, status classification, schema decoding, and typed error mapping.

**Anti-Pattern - Direct fetch/axios:**

```typescript
// ❌ WRONG - No Effect integration, manual error handling
declare const fetch: (url: string) => Promise<{ json: () => Promise<unknown> }>;

const response = await fetch('https://api.example.com/data');
const data = await response.json();

// ❌ WRONG - External dependency, not in Effect system
import axios from 'axios';

declare const axios: {
	get: (url: string) => Promise<{ data: unknown }>;
};

const result = await axios.get('https://api.example.com/data');
```

**Correct Pattern - HttpClient Service:**

```typescript
import { HttpClient, HttpClientResponse } from 'effect/unstable/http';
import { Effect, Schema } from 'effect';

// ✅ CORRECT - Integrated with Effect type system
class ProviderData extends Schema.Class<ProviderData>('ProviderData')({
	value: Schema.String
}) {}

const fetchData = Effect.gen(function* () {
	const client = yield* HttpClient.HttpClient;

	return yield* client.get('https://api.example.com/data').pipe(
		Effect.flatMap(HttpClientResponse.filterStatusOk),
		Effect.flatMap(HttpClientResponse.schemaBodyJson(ProviderData))
	);
});
```

Name the adapter service and its effects after the upstream operation. The adapter owns request/auth construction, executes outside database transactions, classifies status before decoding, validates unknown bodies with `Schema`, and maps failures into typed domain errors. Preserve bounded evidence such as status, provider error code, request ID, and retry metadata, but redact credentials, private fields, query secrets, and full response bodies.

**Advanced HTTP Operations:**

```typescript
import {
	HttpClient,
	HttpClientRequest,
	HttpClientResponse
} from 'effect/unstable/http';
import { Effect, Schema, Schedule } from 'effect';

class User extends Schema.Class<User>('User')({
	id: Schema.Number,
	name: Schema.String,
	email: Schema.String
}) {}

const httpExamples = Effect.gen(function* () {
	const client = yield* HttpClient.HttpClient;

	// GET with query parameters
	const getUsers = client.get('https://api.example.com/users', {
		urlParams: { page: '1', limit: '10' }
	});

	// POST with JSON body
	const createUser = HttpClientRequest.post(
		'https://api.example.com/users'
	).pipe(
		HttpClientRequest.bodyJsonUnsafe({
			name: 'John Doe',
			email: 'john@example.com'
		}),
		client.execute
	);

	// Custom headers — construct the request, set headers, then execute
	const withAuthRequest = HttpClientRequest.get(
		'https://api.example.com/protected'
	).pipe(HttpClientRequest.setHeader('Authorization', 'Bearer token'));
	const withAuth = client.execute(withAuthRequest);

	// Classify status, then parse the successful response with Schema
	const users = yield* client
		.get('https://api.example.com/users')
		.pipe(
			Effect.flatMap(HttpClientResponse.filterStatusOk),
			Effect.flatMap(
				HttpClientResponse.schemaBodyJson(Schema.Array(User))
			)
		);

	// Error handling — all HttpClient errors are "HttpClientError" with a reason field
	const safeRequest = client.get('https://api.example.com/data').pipe(
		Effect.flatMap(HttpClientResponse.filterStatusOk),
		Effect.catchTag('HttpClientError', (error) => {
			switch (error.reason._tag) {
				case 'TransportError':
					return Effect.succeed({ error: 'Network error' });
				case 'StatusCodeError':
					return Effect.succeed({
						error: `HTTP ${error.response?.status}`
					});
				default:
					return Effect.succeed({
						error: `Client error: ${error.reason._tag}`
					});
			}
		})
	);

	// Retries with backoff: GET is idempotent and attempts are bounded.
	const withRetries = client.get('https://api.example.com/data').pipe(
		Effect.flatMap(HttpClientResponse.filterStatusOk),
		Effect.retry({
			times: 3,
			schedule: Schedule.exponential('100 millis')
		}),
		Effect.tapError((error) =>
			Effect.logError('Provider read exhausted retries').pipe(
				Effect.annotateLogs({ operation: 'Provider.getData', errorTag: error._tag })
			)
		)
	);

	return users;
});
```

Never install retry automatically on a shared client that also sends non-idempotent POST/PATCH requests. Retry only operations proven idempotent by method/provider contract or protected by a provider-supported idempotency key. Keep exhaustion observable: retain the final typed error and log/measure only redacted status, provider code, request ID, operation, and attempt evidence.

`HttpClient.withRateLimiter` can automatically retry 429 responses. Use positive `times` only on a client restricted to proven-idempotent operations; set `times: 0` for mixed/non-idempotent clients so the 429 remains visible.

### KeyValueStore - Key-Value Storage

The `KeyValueStore` service provides platform-independent key-value storage.

**Anti-Pattern - Direct localStorage/file-based storage:**

```typescript
// ❌ WRONG - Browser-specific
declare const localStorage: {
	setItem: (key: string, value: string) => void;
	getItem: (key: string) => string | null;
};

localStorage.setItem('key', 'value');
const value = localStorage.getItem('key');

// ❌ WRONG - Node-specific, manual file handling
import fs from 'fs';

declare const fs: {
	writeFileSync: (path: string, data: string) => void;
	readFileSync: (path: string, encoding: string) => string;
};

fs.writeFileSync('.cache/key', 'value');
const value2 = fs.readFileSync('.cache/key', 'utf-8');
```

**Correct Pattern - KeyValueStore Service:**

```typescript
import { KeyValueStore } from 'effect/unstable/persistence';
import { Effect, Schema } from 'effect';

// ✅ CORRECT - Works on all platforms
const cacheData = Effect.gen(function* () {
	const store = yield* KeyValueStore.KeyValueStore;

	// Set value
	yield* store.set('user:123', 'John Doe');

	// Get value — raw string stores return string | undefined
	const name = yield* store.get('user:123');

	// Binary stores return Uint8Array | undefined
	const bytes = yield* store.getUint8Array('avatar:123');

	// Check existence
	const hasUser = yield* store.has('user:123');

	// Remove value
	yield* store.remove('user:123');

	// Clear all
	yield* store.clear;

	return name;
});
```

**Schema-Based Store:**

```typescript
import { KeyValueStore } from 'effect/unstable/persistence';
import { Effect, Schema } from 'effect';

class User extends Schema.Class<User>('User')({
	id: Schema.Number,
	name: Schema.String,
	email: Schema.String
}) {}

const typedStore = Effect.gen(function* () {
	const store = yield* KeyValueStore.KeyValueStore;

	// Create schema-based store
	const userStore = KeyValueStore.toSchemaStore(store, User);

	// Type-safe operations
	yield* userStore.set('user:123', new User({
		id: 123,
		name: 'John Doe',
		email: 'john@example.com'
	}));

	const user = yield* userStore.get('user:123');
	// user: Option.Option<{ id: number, name: string, email: string }>
});
```

### Redis - Commands and Scoped Subscriptions

The portable `Redis.Redis` service in `effect/unstable/persistence` provides `send`, cached script evaluation, and scoped pub/sub. Platform-specific layers provide the client: `NodeRedis.layer(...)`, `DenoRedis.layer(...)`, or `BunRedis.layer(...)`. These are specialized layers and are not included in `NodeServices.layer` or `BunServices.layer`.

```typescript
import { NodeRedis } from '@effect/platform-node';
import { Effect, Queue } from 'effect';
import { Redis } from 'effect/unstable/persistence';

const RedisLayer = NodeRedis.layer({
	database: 1,
	socket: { host: '127.0.0.1', port: 6379 }
});

const receiveOne = Effect.gen(function* () {
	const redis = yield* Redis.Redis;
	const subscription = yield* redis.subscribe('events');
	return yield* Queue.take(subscription);
}).pipe(Effect.scoped, Effect.provide(RedisLayer));
```

`redis.subscribe(channel)` requires `Scope` and returns a `Queue.Dequeue<RedisMessage, RedisError>`. Closing the scope shuts down the queue and releases the dedicated subscriber. Node and Deno subscribers reconnect and re-subscribe after interruptions, which can leave message-delivery gaps; Bun subscriptions do not reconnect, so a dropped connection fails the dequeue and callers must subscribe again.

### CLI Arguments - effect/unstable/cli

For CLI applications, use `effect/unstable/cli` instead of direct `process.argv`.

**Anti-Pattern - Direct process.argv:**

```typescript
// ❌ WRONG - Manual parsing, no validation
declare const process: { argv: string[] };

const args = process.argv.slice(2);
const input = args[0];
const verbose = args.includes('--verbose');

// ❌ WRONG - Third-party parser, not Effect-integrated
import yargs from 'yargs';

declare const yargs: (args: string[]) => { argv: Record<string, unknown> };

const argv = yargs(process.argv.slice(2)).argv;
```

**Correct Pattern - effect/unstable/cli:**

```typescript
import { Argument, Command as CliCommand, Flag } from 'effect/unstable/cli';
import { NodeServices, NodeRuntime } from '@effect/platform-node';
import { Console, Effect } from 'effect';

declare const process: { argv: string[] };
declare const someOperation: Effect.Effect<string>;

// ✅ CORRECT - Type-safe CLI with full Effect integration
// Define arguments
const inputArg = Argument.file('input');

// Define flags
const verboseFlag = Flag.boolean('verbose').pipe(Flag.withAlias('v'));

// Define command
const command = CliCommand.make(
	'process',
	{ input: inputArg, verbose: verboseFlag },
	Effect.fn(function* ({ input, verbose }) {
		if (verbose) {
			yield* Console.log(`Processing file: ${input}`);
		}
		// Process the file
		yield* someOperation;
	})
);

// Run CLI
command.pipe(
	CliCommand.run({ version: '1.0.0' }),
	Effect.provide(NodeServices.layer),
	NodeRuntime.runMain
);
```

## Platform Module Reference

Complete reference table of platform abstractions:

| Need                      | Use                                    | Instead of                   | Import from                   |
| ------------------------- | -------------------------------------- | ---------------------------- | ----------------------------- |
| **File I/O**              | `FileSystem.FileSystem`                | `fs`, `Bun.file`             | `effect`                      |
| **Path Operations**       | `Path.Path`                            | `path`, string concat        | `effect`                      |
| **Process Spawning**      | `ChildProcess` + `ChildProcessSpawner` | `child_process`, `Bun.spawn` | `effect/unstable/process`     |
| **Terminal I/O**          | `Terminal.Terminal`                    | `process.stdin/stdout`       | `effect`                      |
| **Console Logging**       | `Console.log` or `Effect.log`          | `console.log`                | `effect`                      |
| **Crypto**                | `Crypto.Crypto`                        | `globalThis.crypto`, `crypto.randomUUID()` | `effect`          |
| **HTTP Client**           | `HttpClient.HttpClient`                | `fetch`, `axios`             | `effect/unstable/http`        |
| **HTTP Server**           | `HttpServer.HttpServer`                | `http.createServer`          | `effect/unstable/http`        |
| **Sockets**               | `Socket.Socket` / `SocketServer.SocketServer` | raw TCP/WebSocket APIs | `effect/unstable/socket`      |
| **Key-Value Store**       | `KeyValueStore.KeyValueStore`          | `localStorage`, manual files | `effect/unstable/persistence` |
| **Redis**                 | `Redis.Redis`                          | direct Redis clients         | `effect/unstable/persistence` |
| **CLI Arguments**         | `Argument` + `Flag` + `Command`        | `process.argv`, `yargs`      | `effect/unstable/cli`         |
| **Environment Variables** | `Config` from effect                   | `process.env`                | `effect`                      |
| **Streams**               | `Stream`                               | Node streams, ReadableStream | `effect`                      |

Socket services live in `effect/unstable/socket`. Use `Socket.Socket` for scoped bidirectional string/binary frame transports and `SocketServer.SocketServer` for accepting connections. Provide service-specific layers such as `BrowserSocket.layerWebSocket(url)`, `NodeSocket.layerWebSocket(url)`, `NodeSocket.layerNet(options)`, `BunSocket.layerWebSocket(url)`, or Node/Bun socket-server layers; `NodeServices.layer` and `BunServices.layer` do not provide sockets.

## Setting Up Platform-Specific Layers

To use platform services, provide the appropriate platform layer.

`NodeServices.layer` and `BunServices.layer` provide core process services such as `FileSystem`, `Path`, `ChildProcessSpawner`, `Stdio`/`Terminal`, and `Crypto.Crypto`. They do **not** provide specialized integrations such as HTTP clients/servers, sockets, workers, or Redis; provide those with service-specific platform layers. For HTTP clients, provide an HTTP-specific layer such as `FetchHttpClient.layer`, Node's `NodeHttpClient.{layerFetch, layerUndici, layerNodeHttp}`, `BunHttpClient.layer`, or Browser's `BrowserHttpClient.{layerFetch, layerXMLHttpRequest}`. A named provider adapter should export a raw `layer` that requires `HttpClient.HttpClient`, plus an optional `defaultLayer = layer.pipe(Layer.provide(...transport...))`; this keeps the transport dependency graph explicit while offering runtime convenience. For HTTP servers, use server layers such as `NodeHttpServer.layer(...)`, `BunHttpServer.layer(...)`, or their `layerHttpServices` variants where appropriate.

**Node.js:**

```typescript
import { NodeServices, NodeRuntime } from '@effect/platform-node';
import { Effect, FileSystem } from 'effect';

const program = Effect.gen(function* () {
	const fs = yield* FileSystem.FileSystem;
	return yield* fs.readFileString('file.txt');
});

program.pipe(Effect.provide(NodeServices.layer), NodeRuntime.runMain);
```

**Bun:**

```typescript
import { BunServices, BunRuntime } from '@effect/platform-bun';
import { Effect, FileSystem } from 'effect';

const program = Effect.gen(function* () {
	const fs = yield* FileSystem.FileSystem;
	return yield* fs.readFileString('file.txt');
});

program.pipe(Effect.provide(BunServices.layer), BunRuntime.runMain);
```

## Complete Example: Cross-Platform File Processor

```typescript
import { NodeServices, NodeRuntime } from '@effect/platform-node';
import { BunServices, BunRuntime } from '@effect/platform-bun';
import { Console, Effect, FileSystem, Path, Schema } from 'effect';
import { ChildProcess, ChildProcessSpawner } from 'effect/unstable/process';

class FileProcessorConfig extends Schema.Class<FileProcessorConfig>(
	'FileProcessorConfig'
)({
	inputDir: Schema.String,
	outputDir: Schema.String,
	compress: Schema.Boolean
}) {}

const processFiles = Effect.gen(function* () {
	const fs = yield* FileSystem.FileSystem;
	const path = yield* Path.Path;
	const spawner = yield* ChildProcessSpawner.ChildProcessSpawner;

	// Load configuration
	const configData = yield* fs.readFileString('config.json');
	const config = yield* Schema.decodeUnknownEffect(
		Schema.fromJsonString(FileProcessorConfig)
	)(configData);

	// Ensure output directory exists
	yield* fs.makeDirectory(config.outputDir, { recursive: true });

	// Read input files
	const files = yield* fs.readDirectory(config.inputDir);

	yield* Console.log(`Processing ${files.length} files...`);

	// Process each file
	yield* Effect.forEach(
		files,
		(file) =>
			Effect.gen(function* () {
				const inputPath = path.join(config.inputDir, file);
				const outputPath = path.join(config.outputDir, file);

				// Copy file
				yield* fs.copy(inputPath, outputPath);

				// Optionally compress
				if (config.compress) {
					yield* spawner.string(
						ChildProcess.make('gzip', [outputPath])
					);
				}

				yield* Console.log(`Processed: ${file}`);
			}),
		{ concurrency: 4 }
	);

	yield* Console.log('All files processed!');
});

// Run on Node.js
processFiles.pipe(Effect.provide(NodeServices.layer), NodeRuntime.runMain);

// Or run on Bun - same code!
processFiles.pipe(Effect.provide(BunServices.layer), BunRuntime.runMain);
```

## Testing with Platform Abstractions

One major benefit of platform abstractions is testability:

```typescript
import { Effect, FileSystem, Layer } from 'effect';

declare const myFileProcessor: Effect.Effect<
	void,
	never,
	FileSystem.FileSystem
>;

// Create mock FileSystem using makeNoop for testing
const TestFileSystem = Layer.succeed(
	FileSystem.FileSystem,
	FileSystem.makeNoop({
		readFile: (path) => {
			if (path === 'config.json') {
				const data = JSON.stringify({ key: 'value' });
				return Effect.succeed(new TextEncoder().encode(data));
			}
			return Effect.fail(new Error('File not found'));
		},
		exists: (path) => Effect.succeed(true)
	})
);

// Test your code
const testProgram = myFileProcessor.pipe(Effect.provide(TestFileSystem));

Effect.runPromise(testProgram);
```

## Quality Checklist

Before completing code that uses platform operations:

- [ ] All file I/O uses `FileSystem.FileSystem` service
- [ ] All path operations use `Path.Path` service
- [ ] Process spawning uses `ChildProcess` + `ChildProcessSpawner`
- [ ] Console output uses `Console.log` or `Effect.log` (not `console.log`)
- [ ] CLI arguments parsed with `effect/unstable/cli` (not `process.argv`)
- [ ] HTTP requests use `HttpClient.HttpClient` (not `fetch`/`axios`)
- [ ] Any raw `fetch` is isolated in a named low-level platform adapter with documented justification
- [ ] HTTP status is classified before success-body schema decoding
- [ ] Provider evidence is bounded and redacted; retry exhaustion remains a typed, observable failure
- [ ] Provider/network calls execute outside database transactions
- [ ] Automatic retries apply only to operations proven idempotent
- [ ] Adapter `layer` keeps `HttpClient.HttpClient` visible; optional `defaultLayer` owns transport wiring
- [ ] Cryptographic operations use `Crypto.Crypto` (not direct platform crypto APIs)
- [ ] Platform services accessed through Effect type system
- [ ] Appropriate platform/service layer provided (HTTP, sockets, workers, Redis, and other specialized integrations need service-specific layers, not just `NodeServices.layer` / `BunServices.layer`)
- [ ] No direct imports from `fs`, `path`, `child_process`, `http`, etc.
- [ ] No Bun-specific APIs (`Bun.file`, `Bun.spawn`, etc.)
- [ ] No browser-specific APIs without platform abstraction
- [ ] Code is testable with mock platform services

## Common Mistakes to Avoid

### 1. Mixing Platform APIs

```typescript
import { Effect, FileSystem } from 'effect';
import fs from 'fs';

// ❌ WRONG - Mixing Effect Platform with direct APIs
const bad = Effect.gen(function* () {
	const filesystem = yield* FileSystem.FileSystem;
	const content1 = yield* filesystem.readFileString('file1.txt');
	const content2 = fs.readFileSync('file2.txt', 'utf-8'); // Don't mix!
});

// ✅ CORRECT - Use platform abstractions consistently
const good = Effect.gen(function* () {
	const fs = yield* FileSystem.FileSystem;
	const content1 = yield* fs.readFileString('file1.txt');
	const content2 = yield* fs.readFileString('file2.txt');
});
```

### 2. Forgetting Platform Layer

```typescript
import { NodeServices, NodeRuntime } from '@effect/platform-node';
import { Effect, FileSystem } from 'effect';

// ❌ WRONG - No platform layer provided
const program = Effect.gen(function* () {
	const fs = yield* FileSystem.FileSystem;
	return yield* fs.readFileString('file.txt');
});

Effect.runPromise(program); // Runtime error!

// ✅ CORRECT - Provide platform layer
program.pipe(Effect.provide(NodeServices.layer), NodeRuntime.runMain);
```

### 3. Using console.log

```typescript
import { Console, Effect } from 'effect';

declare const someOperation: () => Effect.Effect<string>;

// ❌ WRONG - Direct console usage
const badProgram = Effect.gen(function* () {
	console.log('Starting...');
	const result = yield* someOperation();
	console.log('Done!');
	return result;
});

// ✅ CORRECT - Use Console or Effect.log
const goodProgram = Effect.gen(function* () {
	yield* Console.log('Starting...');
	const result = yield* someOperation();
	yield* Console.log('Done!');
	return result;
});
```

## Migration Guide

### From Node.js fs to FileSystem

```typescript
import { Effect, FileSystem } from 'effect';
import fs from 'fs/promises';

// Before (Node.js)
declare const fs: {
	readFile: (path: string, encoding: string) => Promise<string>;
	writeFile: (path: string, data: string) => Promise<void>;
	existsSync: (path: string) => boolean;
};

const data = await fs.readFile('file.txt', 'utf-8');
await fs.writeFile('output.txt', data);
const exists = fs.existsSync('config.json');

// After (Effect Platform)
const program = Effect.gen(function* () {
	const fs = yield* FileSystem.FileSystem;

	const data = yield* fs.readFileString('file.txt');
	yield* fs.writeFileString('output.txt', data);
	const exists = yield* fs.exists('config.json');
});
```

### From fetch to HttpClient

```typescript
import { Effect, Schema } from 'effect';
import {
	HttpClient,
	HttpClientRequest,
	HttpClientResponse
} from 'effect/unstable/http';

// Before (fetch)
declare const fetch: (
	url: string,
	options: {
		method: string;
		headers: Record<string, string>;
		body: string;
	}
) => Promise<{ json: () => Promise<unknown> }>;

const response = await fetch('https://api.example.com/data', {
	method: 'POST',
	headers: { 'Content-Type': 'application/json' },
	body: JSON.stringify({ key: 'value' })
});
const data = await response.json();

// After (Effect HttpClient)
class CreateData extends Schema.Class<CreateData>('CreateData')({
	key: Schema.String
}) {}

class CreatedData extends Schema.Class<CreatedData>('CreatedData')({
	id: Schema.String,
	key: Schema.String
}) {}

const program = Effect.gen(function* () {
	const client = yield* HttpClient.HttpClient;

	return yield* HttpClientRequest.post('https://api.example.com/data').pipe(
		HttpClientRequest.schemaBodyJson(CreateData)(new CreateData({ key: 'value' })),
		Effect.flatMap(client.execute),
		Effect.flatMap(HttpClientResponse.filterStatusOk),
		Effect.flatMap(HttpClientResponse.schemaBodyJson(CreatedData))
	);
});
```

This POST is intentionally not retried. Add retry only if the provider offers a documented idempotency guarantee and the request supplies the required idempotency key.

### From child_process to ChildProcess

```typescript
import { Effect } from 'effect';
import { ChildProcess, ChildProcessSpawner } from 'effect/unstable/process';
import { exec } from 'child_process';
import { promisify } from 'util';

// Before (child_process)
declare const exec: (
	cmd: string,
	callback: (error: Error | null, result: { stdout: string }) => void
) => void;
declare const promisify: <T>(fn: T) => (...args: any[]) => Promise<any>;

const execAsync = promisify(exec);
const { stdout } = await execAsync('git status');

// After (Effect ChildProcess)
const program = Effect.gen(function* () {
	const spawner = yield* ChildProcessSpawner.ChildProcessSpawner;
	const stdout = yield* spawner.string(ChildProcess.make('git', ['status']));
	return stdout;
});
```

## Summary

Effect provides a complete abstraction layer over platform-specific APIs, enabling you to:

1. **Write once, run anywhere** - Same code works on Node.js and Bun
2. **Type-safe operations** - All errors tracked in Effect type signatures
3. **Resource safety** - Automatic cleanup with Scope
4. **Easy testing** - Mock services without touching the filesystem
5. **Full Effect integration** - Compose with services, layers, and error handling

Always prefer Effect platform abstractions over direct platform APIs for maximum portability, safety, and testability.

