reliant-labs/forge/services

Claude Code and agent skill by reliant-labs/forge

Trust tier 3
  • community
Source check pending

Checking that the SKILL.md file is still in the repository.

Scaffold and wire new services, packages, and frontends in a Forge project.

Install

npx vskill@latest install reliant-labs/forge/services

SKILL.md

Adding New Components to a Forge Project

Use this skill whenever you need to introduce a new network-facing service, internal package, or frontend into a Forge mono-repo. Every scaffolded service comes with production middleware baked in — structured logging, distributed tracing, auth, rate limiting, and graceful shutdown — so you focus on business logic from day one.

Choosing the Right Command

I need…CommandWhat it creates
A new network-facing API (Connect RPC)forge scaffold service <name>Proto definition, generated stubs, Go service skeleton
A background workerforge scaffold worker <name>Worker with Start/Stop lifecycle
A cron-scheduled workerforge scaffold worker <name> --kind cron --schedule "..."Worker with cron scheduler
A one-shot step that must finish before something else startsdeclare a kind = "job" workload in deploy/kcl/workloads.k and bind it per env (see below)Ordering primitive — no scaffold needed
An internal Go package with interface contractforge package new <name>Package directory with contract interface and default implementation
A Next.js web frontendforge scaffold frontend <name>Next.js app wired into the project
A React Native mobile frontendforge scaffold frontend <name> --kind mobileExpo app with Connect-web transport

The fast path: scaffold everything the protos imply

The per-component commands above are the granular path. When you author the proto directly, there is a one-command batch alternative: mark each entity message with a leading // forge:entity comment, add your custom RPCs, then run `forge scaffold`. It births every marked entity (missing CRUD quintet injected + owned create-table migration) and runs generate, which emits a pb-through handler stub for every custom RPC — in one visible, phased run (--dry-run plans; a re-run with nothing missing is a clean no-op). In dev the app boots alive: forge env up auto-seeds a fresh DB from the applied schema. The entity/seed halves live in the db skill, the pb-through RPC handlers in api, and the standalone domain package (when you extract one) in service-layer / contracts.

One-shot steps: the job workload kind

When something must run once to completion before something else starts — register an OAuth client against the IdP, provision a bucket, load a fixture — that is a job. Every other workload kind is long-running: service / worker / operator run forever, cron runs on a schedule forever, and a tool is never scheduled at all. job is the one that ends.

Declare it ONCE in deploy/kcl/workloads.k, like every workload, and bind it in each env that runs it. Ordering (before) is part of the declaration; WHERE it runs is the env's binding:

kcl
# deploy/kcl/workloads.k
provision_idp = fw.Workload {
name = "provision-idp"
kind = "job"
build = forge.GoBuild {cmd = "./cmd/myproj", output_name = "myproj"}
args = ["provision-idp"] # a subcommand of the project binary
before = ["api"] # the API does not start until this exits 0
}
# deploy/kcl/dev/main.k — one binding line
_workloads = [
_on_host_job(wl.provision_idp)
_on_host(_api) # the binary's `server`: every service
]

before is the ordering declaration and it is the point — a one-shot with no ordering is just a cron that fires once. Each runtime honours it:

runtimelowering
cluster (forge.OnCluster)an init container on each workload named in before, rendered by pkg/deploy.RenderWorkloads. It is the only ordering Kubernetes enforces itself, so it holds under any applier. A gating job renders no standalone Job — that would run the command a second time, unordered.
cluster, before emptya standalone batch/v1 Job, pre-rollout by default (forge env deploy waits for it before applying Deployments); deployPhase = "post-rollout" defers it.
hosted (forge.OnHosted)the control plane runs it the same way, before the workloads it gates.
host (forge.OnHost)forge env up runs go run ./cmd/<p> <args>, waits for exit 0, and only then launches the dependents. Fail-closed: a job that exits non-zero stops the up.
compose (forge.OnCompose)the compose file owns it: restart: "no" plus depends_on: {<job>: {condition: service_completed_successfully}} on each dependent.

The job's command must be idempotent. The k8s lowering fans it out to one init container per dependent, and init containers re-run on every pod start, every replica and every restart. That is the same contract the deploy-time migration step already keeps (migratekit takes a postgres advisory lock), and a one-shot that is not safe to repeat is unsafe under any retrying orchestrator.

Things forge rejects at load time rather than at 3am: a before naming a component that does not exist, a job with no command, a job carrying a schedule (use kind = "cron"), a workload an env lists without binding a runtime, and — on the host path — a cycle in the job graph.

Wiring Cycle

Follow this sequence every time you scaffold a new component:

  1. Scaffold — run the appropriate forge scaffold or forge package new command.
  2. Define the contract — edit the .proto file (services) or the interface (packages).
  3. Generate — run forge generate to produce Go code from protos and contracts (handler stubs, mocks, Connect clients).
  4. Implement — write the business logic on the *Service methods in internal/handlers/<svc>/ using s.deps (a custom RPC is a pb-through method on *Service; CRUD delegates via handlers_crud.go). For a reusable, transport-free domain layer, scaffold a standalone package (forge scaffold package <name>) and call it from the handler.
  5. Compose it into a binary — a binary serves a service because NewComponents constructs it and the serve path mounts it. Add the constructor call to the composition (see below); selection is code, not a string table.

Deploying it: declared once, bound per env

forge scaffold operator|binary appends the workload to deploy/kcl/workloads.k — build, the component's subcommand in args. A service or worker on the composition graph gets NO workload of its own: the binary has no command per service or worker (see "Commands are roots" below), so a stanza with args = ["<service>"] would crash-loop on "unknown command", and forge lint warns about one (component-drift/no-such-command). Every env — dev (a host process under air, hot reload) and staging/prod (hosted on Reliant as scaffolded) — runs every service and worker in ONE workload, _api: the binary's server, every service on one Connect mux, so the browser reaches them all at one origin. A new one needs no binding line (_api is bound if it was not). An operator binds _on_k3d in dev and _on_cluster in deployed envs, since hosting refuses it; see deploy/hosting. Probes need nothing: a service forge builds gets /readyz + /healthz on its http port on every runtime. See the deploy skill for the runtimes and how to rebind one workload (to a cluster you operate, say).

A project that runs its components in processes it composes itself — one binary whose own subcommands (public-api, admin-api, workers) each mount several, so most components of a kind have no workload of their own — gets no workload for a new one: the scaffold prints which processes it found and the stanza, and leaves the choice to you. Mount it in the subcommand that should run it and add it to that workload's serves, or paste the stanza to give it its own. forge lint's component-drift reads the file the same way.

Port Assignment

In a container every service owns :8080 (the http port). In the dev loop each host process gets its own port from plugin.resolve_port in deploy/kcl/dev/main.k. Do not hard-code port numbers elsewhere.

Rules

  • Always use `forge scaffold` or `forge package new` — never copy-paste an existing service or package directory.
  • One service per proto package — keep proto definitions focused on a single domain.
  • Run `forge generate` after any proto or contract change — generated code must stay in sync.
  • Service names canonicalize the same way worker names do: lowercase snake_case (hyphens → underscores, PascalCase boundaries split). forge scaffold service admin-server keeps admin-server as the forge.yaml name: display key, but the on-disk leaf, Go package decl, and forge.yaml path: leaf are all admin_server (internal/handlers/admin_server/, package admin_server, path: internal/handlers/admin_server). See the workers skill Naming section for the full rule and the migration gotcha; see architecture for the cross-ecosystem naming-conventions table.
  • Service code lives under `internal/handlers/<svc>/` — service.go (Deps/New), the rpc methods that hold its logic, and the generated CRUD ops co-located in ONE directory, with no contract.go. The handlers/ role subtree is under internal/, not top-level; a service is app-internal, imported by nobody external.

Serving a service = composing it (the composition root)

What a binary serves is the set of constructors it calls — not a string row in a registry. The explicit composition is split across two files under internal/app/, both yours: providers.go (Infra + OpenInfra) and compose.go (Components + NewComponents(infra *Infra) (*Components, error)). A binary serves a service because NewComponents constructs it and the serve path mounts its handler:

go
// internal/app/compose.go (yours; reconciled — newly discovered components and
// Deps keys are injected, your value expressions are kept)
func NewComponents(infra *Infra) (*Components, error) {
c := &Components{}
c.Users = user.New(user.Deps{Repo: infra.Repo})
c.Bill = billing.New(billing.Deps{Users: c.Users}) // dep by INTERFACE, in-process default
// two-phase (bill.WithReliantAPIKeyIssuer(infra.LLM)) → just add it here; it is kept
return c, nil
}

The serve path (cmd/<bin>/cmd/serve.go) then applies the typed Mount<Svc> method values onto a serverkit.Server and calls srv.RequireMounted(...).

  • Per-service handler options are a value, passed explicitly. Every service shares the serve path's interceptor chain. To give one service more (an extra interceptor, its own read limit, its own HTTP stack), build the generated app.MountOptions — one []mountkit.Option field per service — in owned code and name one of its methods as the mount: ServeSpec{Mount: app.MountOptions{Worktree: []mountkit.Option{mountkit.WithHandlerOptions(connect.WithInterceptors(wake))}}.MountAll, ...} in server.go, or .MountWorktree in that service's subcommand. mountkit.WithHandlerOptions comes after the shared chain, so the shared interceptors wrap it.

This is the start of the file. Read the full SKILL.md on GitHub.

Security

  • Security scanPassed, 95 of 100Pattern checks on the skill's files.
  • Model reviewPassed

Quality evaluation

  • Task evaluationNot runNo evaluation yet. Run one in Skill Studio.

Source

Popularity

Repository stars
4
Repository forks
1
7-day trend score
8

Stars and forks are for the whole GitHub repository, not this skill alone. Refreshed 29 Apr 2026, 6 months ago.

Works with

Any agent vskill installs to: 56 targets in all. These 39 have a page on this site.

  • Claude Code
  • Cursor
  • GitHub Copilot
  • Windsurf
  • Codex
  • Gemini CLI
  • Cline
  • Amp
  • Roo Code
  • Goose
  • Continue
  • Kilo Code
  • Junie
  • Trae
  • OpenHands
  • Qwen Code
  • AI Agent
  • Tabnine Chat
  • Sourcegraph Cody
  • Amazon Q
  • Firebase Genkit
  • Sweep
  • Devin
  • Phind
  • Mentat
  • Claude Desktop
  • Aider
  • GPT Pilot
  • Plandex
  • bolt.new
  • v0
  • Replit Agent
  • Anse
  • TypingMind
  • BetterChatGPT
  • LibreChat
  • Warp AI
  • Pieces
  • Fig AI
Embed badge
vskill VERIFIED badge
[![vskill VERIFIED](https://verified-skill.com/api/v1/skills/reliant-labs/forge/services/badge)](https://verified-skill.com/skills/reliant-labs/forge/services)

Something wrong with this skill or its page? Tell us.