# `@geenius/adapters`

Typed adapter contracts for the Geenius ecosystem. The current package surface is a contract layer for infrastructure domains and launch DB backends: interfaces, Zod schemas, typed errors, SDK-free DB binding helpers, memory/local dev adapters, and retained UI compatibility subpaths.

<!-- team:body:start -->

## Install

```bash
pnpm add @geenius/adapters
```

Install only the peers for the subpaths you import:

| Subpath | Styling/runtime | Required peers |
|---|---|---|
| `@geenius/adapters` | Framework-agnostic contracts | None |
| `@geenius/adapters/storage` | Object/blob storage contract | None |
| `@geenius/adapters/cache` | Key/value cache contract | None |
| `@geenius/adapters/queue` | Job queue and scheduled-work contract | None |
| `@geenius/adapters/events` | Pub/sub and webhook contract | None |
| `@geenius/adapters/auth` | Session and identity-provider contract | None |
| `@geenius/adapters/payments` | Payment-provider contract | None |
| `@geenius/adapters/deploy` | Deployment-platform contract | None |
| `@geenius/adapters/react` | React, Tailwind CSS v4 | `react`, `react-dom` |
| `@geenius/adapters/react-css` | React, Vanilla CSS | `react`, `react-dom` |
| `@geenius/adapters/react-shadcn` | React, shadcn/Radix-compatible bridge | `react`, `react-dom` |
| `@geenius/adapters/react-ant` | React, Ant Design bridge | `react`, `react-dom`, `antd`, `@ant-design/icons` |
| `@geenius/adapters/react-chakra` | React, Chakra-compatible bridge | `react`, `react-dom` |
| `@geenius/adapters/react-mui` | React, MUI-compatible bridge | `react`, `react-dom` |
| `@geenius/adapters/react-mantine` | React, Mantine-compatible bridge | `react`, `react-dom` |
| `@geenius/adapters/react-heroui` | React, HeroUI-compatible bridge | `react`, `react-dom` |
| `@geenius/adapters/react-daisyui` | React, DaisyUI-compatible bridge | `react`, `react-dom` |
| `@geenius/adapters/react-native` | React Native bridge | `react`, `react-native` |
| `@geenius/adapters/solidjs` | SolidJS, Tailwind CSS v4 | `solid-js` |
| `@geenius/adapters/solidjs-css` | SolidJS, Vanilla CSS | `solid-js` |
| `@geenius/adapters/solidjs-ark` | SolidJS, Ark-compatible bridge | `solid-js` |
| `@geenius/adapters/solidjs-kobalte` | SolidJS, Kobalte-compatible bridge | `solid-js` |
| `@geenius/adapters/solidjs-solidui` | SolidJS, SolidUI-compatible bridge | `solid-js` |
| `@geenius/adapters/convex` | Convex DB binding helpers | `convex` plus the downstream `@geenius/db` Convex adapter you bind |
| `@geenius/adapters/neon` | Neon DB binding helpers | The downstream `@geenius/db` Neon adapter you bind |
| `@geenius/adapters/cloudflareKV` | Cloudflare Workers KV binding helpers | A Workers KV namespace |
| `@geenius/adapters/memory` | In-process DB adapter | None |

## Quick Start

Use domain subpaths when a consumer only needs a contract:

```ts
import type {
  AuthAdapter,
  AuthProviderAdapter,
} from '@geenius/adapters/auth'
import type { CacheAdapter } from '@geenius/adapters/cache'
import type { DeployAdapter } from '@geenius/adapters/deploy'
import type {
  EventEnvelope,
  EventPublishInput,
  EventsAdapter,
} from '@geenius/adapters/events'
import type { PaymentsProviderAdapter } from '@geenius/adapters/payments'
import type { QueueAdapter } from '@geenius/adapters/queue'
import type { AdapterHealth, StorageAdapter } from '@geenius/adapters/storage'
import {
  AuthContractError,
  AuthIdentitySchema,
} from '@geenius/adapters/auth'
import {
  CacheEntrySchema,
  CacheContractError,
  CacheSetOptionsSchema,
} from '@geenius/adapters/cache'
import {
  DeployContractError,
  DeploymentResultSchema,
  DeploymentTargetSchema,
} from '@geenius/adapters/deploy'
import {
  AdapterHealthSchema as EventsHealthSchema,
  EventEnvelopeSchema,
  EventPublishInputSchema,
  EventSubscribeInputSchema,
  EventsContractError,
  WebhookDeliverySchema,
  WebhookEmitInputSchema,
} from '@geenius/adapters/events'
import {
  PaymentCheckoutInputSchema,
  PaymentCheckoutSessionSchema,
  PaymentPlanContractSchema,
  PaymentSubscriptionContractSchema,
  PaymentsContractError,
} from '@geenius/adapters/payments'
import {
  QueueContractError,
  QueueJobInputSchema,
} from '@geenius/adapters/queue'
import {
  StorageContractError,
  StorageError,
  StorageObjectSchema,
  StorageWriteInputSchema,
} from '@geenius/adapters/storage'

export interface AppAdapters {
  auth: AuthAdapter
  authProvider: AuthProviderAdapter
  cache: CacheAdapter
  deploy: DeployAdapter
  events: EventsAdapter
  paymentsProvider: PaymentsProviderAdapter
  queue: QueueAdapter
  storage: StorageAdapter
}

export function parseCacheEntry(input: unknown) {
  const result = CacheEntrySchema.safeParse(input)

  if (!result.success) {
    throw new CacheContractError('Invalid cache entry', result.error)
  }

  return result.data
}

export async function setCachedValue(
  cache: CacheAdapter,
  key: string,
  value: unknown,
  ttlSeconds: number,
) {
  const result = CacheSetOptionsSchema.safeParse({ ttlSeconds })

  if (!result.success) {
    throw new CacheContractError('Invalid cache set options', result.error)
  }

  await cache.set(key, value, result.data)
}

export async function deployValidatedTarget(
  deploy: DeployAdapter,
  input: unknown,
) {
  const result = DeploymentTargetSchema.safeParse(input)

  if (!result.success) {
    throw new DeployContractError('Invalid deployment target', result.error)
  }

  const deployment = await deploy.deploy(result.data)
  const deploymentResult = DeploymentResultSchema.safeParse(deployment)

  if (!deploymentResult.success) {
    throw new DeployContractError(
      'Invalid deployment result',
      deploymentResult.error,
    )
  }

  return deploymentResult.data
}

export async function putValidatedObject(
  storage: StorageAdapter,
  input: unknown,
) {
  const result = StorageWriteInputSchema.safeParse(input)

  if (!result.success) {
    throw new StorageContractError('Invalid storage write input', result.error)
  }

  const object = await storage.putObject(result.data)
  return StorageObjectSchema.parse(object)
}

export async function listValidatedObjects(storage: StorageAdapter) {
  const objects = await storage.listObjects({ prefix: 'uploads/', limit: 25 })
  return objects.map((object) => StorageObjectSchema.parse(object))
}

export async function requireStorageObject(
  storage: StorageAdapter,
  key: string,
) {
  const object = await storage.getObject(key)

  if (!object) {
    throw new StorageContractError(`Missing storage object: ${key}`)
  }

  return StorageObjectSchema.parse(object)
}

export async function deleteStoredObject(storage: StorageAdapter, key: string) {
  await storage.deleteObject(key)
}

export async function requireStorageHealth(
  storage: StorageAdapter,
): Promise<AdapterHealth> {
  const health = await storage.getHealth()

  if (!health.ok) {
    throw new StorageError(
      'Storage provider health check failed',
      'SERVICE_ERROR',
    )
  }

  return health
}

export async function listValidatedPlans(payments: PaymentsProviderAdapter) {
  const plans = await payments.listPlans()

  return plans.map((plan) => {
    const result = PaymentPlanContractSchema.safeParse(plan)

    if (!result.success) {
      throw new PaymentsContractError('Invalid payment plan', result.error)
    }

    return result.data
  })
}

export async function createValidatedCheckout(
  payments: PaymentsProviderAdapter,
  input: unknown,
) {
  const inputResult = PaymentCheckoutInputSchema.safeParse(input)

  if (!inputResult.success) {
    throw new PaymentsContractError('Invalid checkout input', inputResult.error)
  }

  const session = await payments.createCheckout(inputResult.data)
  const result = PaymentCheckoutSessionSchema.safeParse(session)

  if (!result.success) {
    throw new PaymentsContractError('Invalid checkout session', result.error)
  }

  return result.data
}

export async function getValidatedSubscription(
  payments: PaymentsProviderAdapter,
  identityId: string,
) {
  const subscription = await payments.getSubscription(identityId)

  if (!subscription) {
    return null
  }

  const result = PaymentSubscriptionContractSchema.safeParse(subscription)

  if (!result.success) {
    throw new PaymentsContractError('Invalid subscription', result.error)
  }

  return result.data
}

export async function cancelValidatedSubscription(
  payments: PaymentsProviderAdapter,
  identityId: string,
) {
  await payments.cancelSubscription(identityId)
  return getValidatedSubscription(payments, identityId)
}

export function requireFeatureEntitlement(
  payments: PaymentsProviderAdapter,
  identityId: string,
  feature: string,
) {
  return payments.isFeatureEnabled(identityId, feature)
}

export async function submitQueueJob(
  queue: QueueAdapter,
  input: unknown,
  runAt?: string,
) {
  const result = QueueJobInputSchema.safeParse(input)

  if (!result.success) {
    throw new QueueContractError('Invalid queue job input', result.error)
  }

  return runAt
    ? queue.schedule({ ...result.data, runAt })
    : queue.enqueue(result.data)
}

export async function publishValidatedEvent(
  events: EventsAdapter,
  input: unknown,
) {
  const result = EventPublishInputSchema.safeParse(input)

  if (!result.success) {
    throw new EventsContractError('Invalid event publish input', result.error)
  }

  const event = await events.publish(result.data)
  return EventEnvelopeSchema.parse(event)
}

export async function emitValidatedWebhook(
  events: EventsAdapter,
  input: EventPublishInput,
  url: string,
) {
  const event = await publishValidatedEvent(events, input)
  const webhookInput = WebhookEmitInputSchema.parse({ url, event })
  const delivery = await events.emitWebhook(webhookInput)

  return WebhookDeliverySchema.parse(delivery)
}

export async function subscribeToValidatedEvents(
  events: EventsAdapter,
  topic: string,
  handler: (event: EventEnvelope) => void | Promise<void>,
) {
  const subscribeInput = EventSubscribeInputSchema.parse({ topic })
  const subscription = await events.subscribe(subscribeInput, (event) => {
    return handler(EventEnvelopeSchema.parse(event))
  })

  return {
    unsubscribe: () => subscription.unsubscribe(),
  }
}

export async function listValidatedEvents(
  events: EventsAdapter,
  prefix?: string,
) {
  const envelopes = await events.listEvents({ prefix, limit: 25 })
  return envelopes.map((event) => EventEnvelopeSchema.parse(event))
}

export async function requireEventsHealth(events: EventsAdapter) {
  const health = EventsHealthSchema.parse(await events.getHealth())

  if (!health.ok) {
    throw new EventsContractError(health.message ?? 'Events adapter unhealthy')
  }

  return health
}

export async function requireIdentity(
  auth: AuthProviderAdapter,
  identityId: string,
) {
  const identity = await auth.getIdentity(identityId)

  if (!identity) {
    throw new AuthContractError(`Unknown identity: ${identityId}`)
  }

  return AuthIdentitySchema.parse(identity)
}
```

Create the shared adapter set once, then pass it to the framework provider.

```ts
import {
  configureAdapters,
  createConsoleLoggerAdapter,
  createLocalStorageAuthAdapter,
  createLocalStorageDbAdapter,
} from '@geenius/adapters'

configureAdapters({
  db: { provider: 'localStorage' },
  auth: { provider: 'localStorage' },
  logger: { provider: 'console' },
})

export const adapters = {
  db: createLocalStorageDbAdapter(),
  auth: createLocalStorageAuthAdapter(),
  logger: createConsoleLoggerAdapter(),
}
```

React:

```tsx
import {
  AdapterList,
  AdapterProvider,
  useAdapterStatuses,
  useDb,
} from '@geenius/adapters/react'
import { adapters } from './adapters'

function Dashboard() {
  const db = useDb()
  const statuses = useAdapterStatuses()

  void db.list('users')

  return (
    <>
      <p>Database adapter: {statuses.db.status}</p>
      <AdapterList />
    </>
  )
}

export function App() {
  return (
    <AdapterProvider adapters={adapters} healthCheck>
      <Dashboard />
    </AdapterProvider>
  )
}
```

SolidJS:

```tsx
import {
  AdapterProvider,
  createAuthAdapter,
  createDb,
} from '@geenius/adapters/solidjs'
import { adapters } from './adapters'

function Dashboard() {
  const auth = createAuthAdapter()
  const db = createDb()

  void auth.getSession()
  void db.list('users')

  return null
}

export function App() {
  return (
    <AdapterProvider adapters={adapters} healthCheck>
      <Dashboard />
    </AdapterProvider>
  )
}
```

For Vanilla CSS variants, import the stylesheet next to the component subpath:

```ts
import { AdapterList } from '@geenius/adapters/react-css'
import '@geenius/adapters/react-css/styles.css'
```

## Configuration Patterns

`@geenius/adapters` exposes `configureAdapters()` rather than an exported config
builder. Keep app-level presets as plain objects and validate them through the
package API:

```ts
import type { AdapterConfig } from '@geenius/adapters'
import { configureAdapters, LAUNCH_DB_PROVIDERS } from '@geenius/adapters'

const simple: AdapterConfig = {
  db: { provider: 'localStorage' },
  auth: { provider: 'localStorage' },
  logger: { provider: 'console' },
}

const production: AdapterConfig = {
  db: { provider: LAUNCH_DB_PROVIDERS[0] },
  auth: { provider: 'better-auth' },
  payments: { provider: 'stripe' },
  ai: { provider: 'openai' },
  storage: { provider: 'r2' },
  logger: { provider: 'console' },
}

const isDevelopment = true

configureAdapters(isDevelopment ? simple : production)
```

Use `registerResolver(domain, provider, factory)` when the concrete provider
factory lives in a downstream package such as `@geenius/db`, `@geenius/auth`,
`@geenius/payments`, `@geenius/ai`, or `@geenius/file-storage`.

## Provider Registry

`@geenius/adapters` owns two related contract surfaces:

- Infrastructure domain contracts: `storage`, `cache`, `queue`, `events`,
  `auth`, `payments`, and `deploy`.
- Launch DB backend subpaths: `convex`, `neon`, `cloudflareKV`, and `memory`.

The older root registry for `db`, `auth`, `payments`, `ai`, `storage`, `admin`,
and `logger` remains exported for UI-provider compatibility. New persistence
runtime implementations belong in `@geenius/db`; new concrete domain provider
SDKs belong in their owning domain package.

The DB provider registry exports current launch ids and future metadata from
`constants.ts`:

| Scope | Providers |
|---|---|
| Launch | `convex`, `neon`, `cloudflareKV`, `memory` |
| Future metadata | `supabase`, `mongodb` |
| Platform-native metadata | launch `convex`, launch `neon`, future `supabase`, future `mongodb` |
| Edge KV metadata | launch `cloudflareKV` |
| In-process metadata | launch `memory` |
| BYOD ORM metadata | `drizzle`, `prisma`, `kysely` |

Use `LAUNCH_DB_PROVIDERS`, `FUTURE_DB_PROVIDERS`,
`ECOSYSTEM_DB_PROVIDERS`, `PLATFORM_NATIVE_DB_PROVIDERS`,
`BYOD_ORM_DB_PROVIDERS`, `getEcosystemDbProviderMeta()`,
`getEcosystemDbProvidersByCategory()`, and
`getEcosystemDbProvidersByStage()` for setup UIs and scaffold tooling.

Only the launch row maps to v1 public provider subpaths. Future and BYOD ORM
entries are root metadata/resolver ids for setup UIs and downstream wiring; a
coordinated ecosystem-axis update is still required before any
`@geenius/adapters/<provider>` import path exists for those ids.

The public Cloudflare KV subpath and provider id are `@geenius/adapters/cloudflareKV`
and `cloudflareKV`.

`neon` is reserved for the native Neon/serverless PostgreSQL adapter contract.
Downstream apps that use Drizzle should bind it behind their Neon adapter module;
`drizzle` is metadata only and is not a current `@geenius/adapters` DB provider
subpath.

## Errors

Adapter failures are surfaced as typed error classes:

- `AdapterError`
- `DbError`
- `AuthError`
- `AiError`
- `StorageError`
- `PaymentsError`
- `AdminError`
- `AdapterConfigurationError`
- `AdapterResolutionError`
- `AdapterContextError`
- `AdapterInvariantError`

Consumers should catch these exported classes instead of vendor-specific exceptions.
Domain subpaths also expose SDK-free contract errors for validation boundaries:
`PaymentsContractError` from `@geenius/adapters/payments` represents invalid
payments contract payloads before a concrete provider runs, while `PaymentsError`
represents root/local runtime payment adapter failures.
`DeployContractError` from `@geenius/adapters/deploy` has the same role for
deployment target and deployment result validation.

```ts
import {
  AdapterConfigurationError,
  AdapterResolutionError,
  configureAdapters,
  isAdapterError,
  withRetry,
} from '@geenius/adapters'

const db = {
  list: async (_collection: string) => [],
}

function reportConfigurationProblem(message: string) {
  console.warn(message)
}

function reportAdapterProblem(domain: string, code: string) {
  console.warn(`${domain}:${code}`)
}

try {
  await withRetry(() => db.list('users'), { maxAttempts: 3 })
} catch (error) {
  if (error instanceof AdapterResolutionError) {
    configureAdapters({ db: { provider: 'localStorage' } })
  } else if (error instanceof AdapterConfigurationError) {
    reportConfigurationProblem(error.message)
  } else if (isAdapterError(error)) {
    reportAdapterProblem(error.domain, error.code)
  } else {
    throw error
  }
}
```

## Convex, Neon, Cloudflare KV, And Memory

The Convex and Neon subpaths do not import provider SDKs. They validate and bind
`DbAdapter` instances produced downstream, usually by `@geenius/db`, and expose
the same SDK-free domain contract exports as the other backend subpaths.
Cloudflare KV stays limited to a Workers KV namespace contract. Memory is the
local/dev implementation surface and also implements every new domain contract
with localStorage-compatible mocks.

```ts
import type { DbAdapter } from '@geenius/adapters'
import { configureAdapters } from '@geenius/adapters'
import { createCloudflareKvDbAdapter } from '@geenius/adapters/cloudflareKV'
import { createMemoryDbAdapter } from '@geenius/adapters/memory'
import { type ConvexProviderContract, withConvexDb } from '@geenius/adapters/convex'
import { type CreateNeonDbAdapterOptions, withNeonDb } from '@geenius/adapters/neon'

interface Env {
  ADAPTERS_KV: KVNamespace
}

declare function createConvexDbAdapter(options: unknown): DbAdapter
declare const neonAdapterFromGeeniusDb: DbAdapter
declare const neonFactoryOptions: CreateNeonDbAdapterOptions
declare const env: Env

const convexOptions = {}

const convexContract: ConvexProviderContract = {
  provider: 'convex',
  stage: 'launch',
  sdk: 'convex',
  schema: 'ConvexDB schema',
  factory: 'createConvexDbAdapter',
  tables: ['users', 'sessions', 'audit_events'],
}
const convexDb = withConvexDb({
  adapter: createConvexDbAdapter(convexOptions),
  provider: 'convex',
  contract: convexContract,
})
const neonDb = withNeonDb({
  adapter: neonAdapterFromGeeniusDb,
  provider: 'neon',
  source: { factoryOptions: neonFactoryOptions },
})
const memoryDb = createMemoryDbAdapter({ environment: 'development' })
const edgeCacheDb = createCloudflareKvDbAdapter({ namespace: env.ADAPTERS_KV })

configureAdapters({
  db: { provider: 'convex' },
})
```

## Storybook Review Apps

The repository retains one review app for each legacy UI variant. For the
current contract-layer mission, `variants.json` marks the five launch review
surfaces as in scope: `react`, `react-css`, `solidjs`, `solidjs-css`, and
`react-native`. The root Storybook build/test scripts intentionally enumerate
every retained review app with a `storybook` entry so backward-compatible UI
subpaths keep review coverage, even when their package source has
`inScope: false`.

- [React review app](./apps/storybook-react)
- [React CSS review app](./apps/storybook-react-css)
- [React shadcn/Radix review app](./apps/storybook-react-shadcn)
- [React Ant Design review app](./apps/storybook-react-ant)
- [React Chakra review app](./apps/storybook-react-chakra)
- [React MUI review app](./apps/storybook-react-mui)
- [React Mantine review app](./apps/storybook-react-mantine)
- [React HeroUI review app](./apps/storybook-react-heroui)
- [React DaisyUI review app](./apps/storybook-react-daisyui)
- [React Native review app](./apps/storybook-react-native)
- [SolidJS review app](./apps/storybook-solidjs)
- [SolidJS CSS review app](./apps/storybook-solidjs-css)
- [SolidJS Ark review app](./apps/storybook-solidjs-ark)
- [SolidJS Kobalte review app](./apps/storybook-solidjs-kobalte)
- [SolidJS SolidUI review app](./apps/storybook-solidjs-solidui)

```bash
pnpm run test:storybook:build
pnpm run test:storybook
```

## Contributing Tests

Variant and provider coverage is driven from [variants.json](./variants.json).
When adding a new launch variant, add its metadata there first, then add the
matching `packages/<variant>/` source, Storybook app, e2e harness coverage, and
bundle budget. Scripts and tests that enumerate variants should import from
`@geenius/release-toolkit` (`packageVariants`, `uiVariants`,
`dbProviderVariants`, …); `pnpm` fan-out uses
`geenius-release pnpm-filters <task> [--sequential …]`.

Use these package-level gates before opening a PR:

```bash
pnpm test:gauntlet
pnpm test:all
pnpm test:visual --update-snapshots
```

`test:gauntlet` is the PR-blocking suite for lint, public package checks,
type-check, unit/root contract tests, bundle budgets, supply-chain audit, and
license validation. `test:all` adds Storybook, DB conformance/migrations,
Playwright e2e, accessibility, visual regression, performance smoke, and
coverage aggregation. Mutation testing runs separately with
`pnpm test:mutation`.

<!-- team:body:end -->

## License

FSL-1.1-Apache-2.0 (free-tier) / Proprietary (commercial)
