Skip to content
Development
Skill

/migrate-orpc-to-effect

Migrate a single oRPC procedure from classic async handler to Effect-backed implementation. Use when converting procedures one at a time to maintain incremental, reviewable changes.

BOOST
From plugin
humanlayer
12k56 skills
Install
$ npx -y skills add humanlayer/humanlayer --skill migrate-orpc-to-effect --agent claude-code

How it fires

How this skill gets triggered: by you, by Claude, or both.

  • Fires itselfAuto-invocation. Claude auto-loads it when your prompt matches the work.Auto-invocation is when the right skill fires by itself at the right moment, driven by a FLOW.md router and a hook, instead of you invoking it by name. It is the difference between a skill being installed and a skill actually getting used.Read the full definition →
  • You can call itInvoke it directly when you want it.
  • Slash command/migrate-orpc-to-effect

Context preview

The summary Claude sees to decide when to auto-load this skill.

Migrate a single oRPC procedure from classic async handler to Effect-backed implementation. Use when converting procedures one at a time to maintain incremental, reviewable changes.

SKILL.md

migrate-orpc-to-effect.SKILL.md
name: migrate-orpc-to-effect
description: Migrate a single oRPC procedure from classic async handler to Effect-backed implementation. Use when converting procedures one at a time to maintain incremental, reviewable changes.
version: "1.1.0"

Migrate oRPC Procedure to Effect

Converts a single classic oRPC procedure to an Effect-backed implementation while preserving route shape, response contract, and authorization semantics.

When to Use

  • Converting procedures that only use services we have Effect wrappers for
  • Incremental Effect migration where you want one procedure per commit/PR
  • Procedure does not already use Effect (no `from 'effect'` import)

Available Effect Services

**IMPORTANT: Always check the source of truth for available services:**

# Check AppRuntimeServices type for available Effect services
cat apps/riptide-api/src/effects/app-runtime.ts | head -50

Only migrate procedures that use services available in `appRuntime`. The `AppRuntimeServices` type in `apps/riptide-api/src/effects/app-runtime.ts` is the authoritative list.

**Key services to look for:**

  • `PostgresDb` - for database access (replaces `context.services.db`)
  • `S3` - for S3 operations (replaces `context.services.s3`)
  • `StripeService` - for Stripe billing operations (replaces eligible `context.services.stripe` usage)
  • Check `effects/services/` directory for other available services

**If a procedure uses a classic service not in `AppRuntimeServices`, do NOT migrate it.**

Prerequisites

Before starting, verify the target procedure is eligible:

# 1. Check what services the procedure uses
rg 'services\.' <file>

# 2. Check what Effect services are available in the runtime
cat apps/riptide-api/src/effects/app-runtime.ts | grep -A20 'AppRuntimeServices'

# 3. Confirm procedure hasn't already been migrated
rg "from 'effect'" <file>
# If matches, procedure is already migrated

If the procedure uses any service from `context.services` that doesn't have a corresponding Effect service in `AppRuntimeServices`, **do NOT migrate it**.

Stripe procedures are eligible only when they can call the public Effect `StripeService` from `apps/riptide-api/src/effects/services/stripe/stripe.service.ts`. Do not call the Stripe TypeScript SDK directly from a migrated oRPC procedure. Load and follow the `effect-program-design` skill for Stripe migrations. `STRIPE_SECRET_KEY` may only be consumed through `Config.redacted` in the Stripe HTTP adapter and must never be printed, inspected, or threaded through procedure code. `kv` remains unavailable; there is no generic `KvService` for migrated procedures.

Migration Workflow

1. Identify the procedure to migrate

Read the source file and understand:

  • Which base it uses (`apiBaseOrgRequired`, `daemonBase`, `apiBaseForOrganizationAdmin`, etc.)
  • The route path (e.g., `comments.delete`, `settings.get`)
  • Input/output contract
  • Authorization requirements
  • Database operations (queries, transactions)
  • Error handling patterns

2. Determine the Effect base

Map the classic base to its Effect equivalent:

| Classic Base | Effect Base | |--------------|-------------| | `apiBaseOrgRequired` | `effectOrpcBaseOrganizationRequired` | | `apiBasePublic` | `effectOrpcBasePublic` | | `apiBaseForOrganizationAdmin` | `effectOrpcBaseForOrganizationAdmin` | | `daemonBase` | `effectOrpcDaemonBase` |

All Effect bases are exported from `apps/riptide-api/src/orpc/effect-base.ts`.

3. Create the Effect procedure file

Create a new file with `.effect.ts` suffix alongside the original:

apps/riptide-api/src/orpc/api/comments/delete.ts      # original
apps/riptide-api/src/orpc/api/comments/delete.effect.ts  # new Effect version

4. Write the Effect procedure

Follow the golden pattern structure:

import { /* drizzle tables */ } from '@humanlayer/db/tables/...'
import { /* drizzle operators */ } from 'drizzle-orm'
import { Effect } from 'effect'
import { mapPostgresErrorToResponse } from '../../../effects/services/postgres/error-mapping'
import { getTransactionId } from '../../../effects/services/postgres/helpers'
import { PostgresDb } from '../../../effects/services/postgres/postgres-db.service'
import { Sentry } from '../../../instrument'
import { effectOrpcBaseOrganizationRequired } from '../../effect-base'

export const deleteCommentEffectProcedure = effectOrpcBaseOrganizationRequired.comments.delete
  .effect(function* ({ input, context, errors }) {
    // 1. Yield services from runtime
    const postgres = yield* PostgresDb

    // 2. Extract auth context
    const { userId, organizationId } = context.auth

    // 3. Implement logic using Effect patterns
    const result = yield* postgres.transaction((tx) =>
      Effect.gen(function* () {
        // Query using tx.query() wrapper
        const [comment] = yield* tx.query(
          tx.client
            .select()
            .from(artifactComments)
            .where(eq(artifactComments.id, input.commentId))
            .limit(1)
        )

        // Fail with oRPC errors using Effect.fail
        if (!comment) {
          return yield* Effect.fail(
            errors.NOT_FOUND({ data: { message: 'Comment not found' } })
          )
        }

        // Continue with business logic...
        
        const transactionId = yield* getTransactionId(tx)
        return { success: true, transactionId }
      })
    ).pipe(
      // Log errors for observability
      Effect.tapError((e) => Effect.logError(e)),
      // Capture to Sentry
      Effect.tapError((e) => Effect.sync(() => Sentry.captureException(e))),
      // Map tagged Postgres errors to oRPC responses
      Effect.catchTags({
        PostgresDbQueryError: (error) => mapPostgresErrorToResponse(error, errors),
        PostgresDbTransactionError: (error) => mapPostgresErrorToResponse(error, errors),
      })
    )

    // 4. Log success with structured context
    yield* Effect.logInfo('Comment deleted').pipe(
Read more
Ships withhumanlayer

HumanLayer runs coding agents (Claude Code, Codex, OpenCode, Pi) on your machines and lets your team steer them from one place: tasks, sessions, plans, and reviews, synced between a desktop app, a web app, and a daemon on each host. This repo holds all of it.

Get the whole plugin

Other skills on humanlayer.