This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
encoded-state (internal name, external TBD) is a TypeScript library for type-safe, versioned query parameters with compression and encryption. Zero build step required for users - they define schemas with Zod at runtime.
Key principle: The library must be polymorphic - work seamlessly in ESM/CommonJS, browser/Node.js, and as drop-in URLSearchParams replacement.
packages/
├── core/ # Main package (@encoded-state/core)
│ ├── src/
│ ├── tests/
│ ├── package.json
│ └── tsconfig.json
├── crypto/ # Encryption (@encoded-state/crypto)
├── migrations/ # Versioning system (@encoded-state/migrations)
├── router/ # Route state encoding (@encoded-state/router)
├── react/ # React hooks (@encoded-state/react)
├── nextjs/ # Next.js integration (@encoded-state/nextjs)
├── remix/ # Remix integration (@encoded-state/remix)
└── sveltekit/ # SvelteKit integration (@encoded-state/sveltekit)
Package scope: @encoded-state/* (internal name, may change before public release)
# Install dependencies
pnpm install
# Run tests across all packages
pnpm test
# Run tests for specific package
pnpm test core
pnpm test crypto
# Watch mode (tests, lint, type-check)
pnpm dev
# Build all packages
pnpm build
# Build specific package
pnpm build --filter @encoded-state/core
# Lint
pnpm lint
# Type check
pnpm type-check- Framework: Vitest
- Location:
packages/*/tests/ - Run single test:
pnpm test core -- my-test.test.ts - Watch mode:
pnpm test --watch
Runtime Dependencies:
zod- Schema definition and validation (peer dependency)protobufjs- Protocol Buffer serialization (dynamic message creation)pako- Compression for browser (zlib wrapper)- Node.js
zlib- Compression for Node.js
Build:
- TypeScript 5.0+ with strict mode
- Must output both ESM and CommonJS
- Must work in browser and Node.js without changes
The library MUST be polymorphic in multiple dimensions:
1. Module Systems
- ESM:
import { createQueryParams } from '@encoded-state/core' - CommonJS:
const { createQueryParams } = require('@encoded-state/core')
2. Runtime Environments
- Browser (modern browsers, last 2 versions)
- Node.js 18+
- Edge runtimes (Cloudflare Workers, Vercel Edge, Deno)
3. String Coercion
const params = createQueryParams(schema);
params.toString() // ✅ Must work
String(params) // ✅ Must work
`/search?${params}` // ✅ Must work in template literals
'' + params // ✅ Must work with concatenationImplement toString() and [Symbol.toPrimitive] for full polymorphism.
4. URLSearchParams Drop-in Replacement Must implement ALL URLSearchParams methods:
get(key),set(key, value),has(key),delete(key)append(key, value),getAll(key)entries(),keys(),values(),forEach(callback)toString(),sort()
Challenge: Convert Zod schemas to protobuf schemas at runtime.
Approach:
- Use
protobufjswith dynamic message creation - Walk Zod schema using internal Zod APIs (or schema introspection)
- Map Zod types to protobuf types:
z.string()→stringz.number()→double(orint32for integers)z.boolean()→boolz.array(T)→repeated Tz.object({})→ nested messagez.optional()→optionalfieldz.union()→oneofor multiple optional fieldsz.enum()→enumorstringwith validationz.record()→map<string, T>
Implementation considerations:
- Cache protobuf message types per schema (don't regenerate)
- Handle nested objects recursively
- Support Zod refinements/transforms at validation layer (not in protobuf)
Default: Automatic compression (always on)
Flow:
- Validate with Zod
- Convert to protobuf message
- Serialize to binary (protobuf)
- Compress with zlib/pako
- Encode as base64url (URL-safe, no padding)
Platform-specific:
- Browser: Use
pako(smaller bundle, pure JS) - Node.js: Use native
zlib(faster, native bindings) - Auto-detect environment and use appropriate implementation
Optimization: For very small payloads (<50 bytes?), compression may increase size. Add tests to determine threshold and potentially skip compression. Document behavior.
Version Storage:
- Add internal field:
_encodedStateVersion(number) - This field name is reserved and cannot be used in user schemas
- Range: 0 to 1023 (2^10 = 1024 versions max)
- Store as part of the protobuf message (not header byte)
Structure:
message EncodedState {
uint32 _encodedStateVersion = 1;
// User fields start at field number 2
}Migration Behavior:
- When parsing, detect version from
_encodedStateVersion - If version < current version: Run up migrations
- If version > current version: Run down migrations
- If no migration defined: Throw error with clear message
- TypeScript: Require all migration paths at compile time
Example:
// v1 -> v2 (up): newer code reading old data
// v2 -> v1 (down): older code reading new data
migrations: {
'v1->v2': {
up: (v1) => ({ ...v1, newField: 'default' }),
down: (v2) => ({ oldField: v2.newField })
}
}Algorithm: AES-256-GCM (authenticated encryption)
- Provides both confidentiality and integrity
- Prevents tampering and replay attacks
Key Management:
- Accept encryption key via:
- Function parameter:
{ encryptionKey: string } - Environment variable:
process.env.ENCODED_STATE_KEY(fallback)
- Function parameter:
- No key rotation support (future feature)
Flow (Secure Mode):
- Encode as normal (Zod → protobuf → compress → base64url)
- Encrypt the entire encoded string with AES-GCM
- Return opaque token (IV + ciphertext + auth tag)
Important: Encrypted tokens are NOT decodeable in browser without key.
Philosophy: Throw errors by default, with safe parse option.
API Design:
// Default: throws on error
const data = params.parse(encoded);
// Safe parse: returns { success, data, error }
const result = params.safeParse(encoded);
if (result.success) {
console.log(result.data);
} else {
console.error(result.error);
}Error Types:
- ValidationError: Zod validation failure
- VersionMismatchError: No migration defined for version
- DecryptionError: Failed to decrypt (wrong key or corrupted)
- DecodingError: Invalid base64url or corrupted data
- CompressionError: Decompression failed
TypeScript Safety:
- If migrations not defined for all version pairs, should be TS error
- Use template literal types to enforce migration keys:
'v1->v2'
tsconfig.json (base):
{
"compilerOptions": {
"target": "ES2020",
"module": "ESNext",
"lib": ["ES2020", "DOM"],
"declaration": true,
"declarationMap": true,
"sourceMap": true,
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"moduleResolution": "bundler",
"resolveJsonModule": true
}
}Dual Package (ESM + CJS):
- Use
tsuporunbuildfor dual builds - Ensure
package.jsonhas correct exports:
{
"type": "module",
"exports": {
".": {
"import": "./dist/index.mjs",
"require": "./dist/index.cjs",
"types": "./dist/index.d.ts"
}
},
"main": "./dist/index.cjs",
"module": "./dist/index.mjs",
"types": "./dist/index.d.ts"
}vitest.config.ts:
import { defineConfig } from 'vitest/config';
export default defineConfig({
test: {
globals: true,
environment: 'node', // or 'jsdom' for browser tests
coverage: {
provider: 'v8',
reporter: ['text', 'json', 'html'],
exclude: ['**/node_modules/**', '**/dist/**', '**/*.test.ts']
}
}
});Test locations: Co-located with source files (e.g., src/encoder.test.ts next to src/encoder.ts)
- Create directory:
packages/my-package/ - Add
package.jsonwith scope:@encoded-state/my-package - Add
tsconfig.jsonextending base config - Add
src/index.tswith exports - Add test files co-located with source:
src/my-feature.test.ts - Update root
pnpm-workspace.yamlif needed - Run
pnpm installto link workspace packages
All packages:
pnpm testSingle package:
pnpm test coreWatch mode (recommended during development):
pnpm dev
# This should run tests, lint, and type-check in watch modeSpecific test file:
pnpm test core -- encoding.test.ts- Prettier: Format code automatically
- ESLint: Enforce code quality rules
- No semicolons (if using Prettier standard config)
- 2 spaces for indentation
- Single quotes for strings
Test Coverage Goals: >90% for core packages
Test Location: Co-located with source files
packages/core/src/
├── encoder.ts
├── encoder.test.ts ← Test next to implementation
├── decoder.ts
├── decoder.test.ts ← Test next to implementation
└── compression/
├── compress.ts
└── compress.test.ts ← Test next to implementation
Test Types:
- Unit tests: Individual functions and classes
- Integration tests: Full encode/decode flows
- Compatibility tests: ESM/CJS, Browser/Node
- Performance tests: Benchmark compression ratios
- Security tests: Encryption/decryption correctness
Test UI: Run pnpm test:ui for interactive web-based test viewer (helpful for debugging)
Important test scenarios:
- String coercion (toString, template literals, String())
- URLSearchParams method compatibility (ALL methods)
- Version migrations (up/down, missing migrations)
- Compression with various payload sizes
- Encryption/decryption round-trips
- Browser vs Node.js environment detection
- Malformed/corrupted input handling
_encodedStateVersion is reserved and MUST NOT be allowed in user schemas.
Validation: Check Zod schema at encoder creation time:
if (schema has field '_encodedStateVersion') {
throw new Error('Field name "_encodedStateVersion" is reserved');
}Document in README: Maximum 1024 versions (2^10) supported.
The safeParse option should be available at encoder creation:
const params = createQueryParams(schema, {
safeParse: true // Returns Result instead of throwing
});Or per-call:
const result = params.parse(encoded, { safe: true });Choose one approach and be consistent across all packages.
- Cache protobuf schemas: Don't regenerate for same Zod schema
- Compression threshold: Test and document when compression is beneficial
- Bundle size: Core package should be <10KB gzipped
- Encoding speed: Target <1ms for typical payloads (<1KB)
Detect environment for compression:
const isNode = typeof process !== 'undefined' &&
process.versions?.node;
const isBrowser = typeof window !== 'undefined';
// Use native zlib in Node, pako in browser- Check if all packages are built:
pnpm build - Check if dependencies are installed:
pnpm install - Check if running correct Node version:
node --version(18+)
- Ensure TypeScript 5.0+:
tsc --version - Rebuild type definitions:
pnpm build - Check workspace links:
pnpm install
- Check
package.jsonexports field - Ensure dual build outputs exist in
dist/ - Test both:
node test.mjsandnode test.cjs
- This is an internal project name (
encoded-state). External package name TBD. - Library is MIT licensed and open source
- Focus on developer experience: zero config, plug-and-play
- Type safety is critical: leverage TypeScript and Zod fully
- Performance is key: compression and encoding should be fast
- Document all edge cases and limitations in README
- Key rotation for encryption
- Custom compression algorithms
- Streaming encode/decode for large payloads
- Browser extension APIs
- React Native support
- Encryption in browser with Web Crypto API (client-side)