@hogsend/cli 0.1.0 → 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (45) hide show
  1. package/dist/bin.js +575 -104
  2. package/dist/bin.js.map +1 -1
  3. package/package.json +4 -1
  4. package/skills/hogsend-authoring-buckets/SKILL.md +69 -0
  5. package/skills/hogsend-authoring-buckets/references/bucket-id-aliases.md +117 -0
  6. package/skills/hogsend-authoring-buckets/references/bucket-meta.md +142 -0
  7. package/skills/hogsend-authoring-buckets/references/buckets-vs-journeys.md +96 -0
  8. package/skills/hogsend-authoring-buckets/references/register-a-bucket.md +129 -0
  9. package/skills/hogsend-authoring-emails/SKILL.md +68 -0
  10. package/skills/hogsend-authoring-emails/references/email-components.md +112 -0
  11. package/skills/hogsend-authoring-emails/references/preview-and-render.md +116 -0
  12. package/skills/hogsend-authoring-emails/references/template-four-file-contract.md +134 -0
  13. package/skills/hogsend-authoring-emails/references/tracking-and-unsubscribe.md +127 -0
  14. package/skills/hogsend-authoring-journeys/SKILL.md +88 -0
  15. package/skills/hogsend-authoring-journeys/references/branch-on-engagement.md +117 -0
  16. package/skills/hogsend-authoring-journeys/references/journey-context.md +133 -0
  17. package/skills/hogsend-authoring-journeys/references/journey-meta.md +145 -0
  18. package/skills/hogsend-authoring-journeys/references/register-a-journey.md +99 -0
  19. package/skills/hogsend-authoring-journeys/references/sending-email-from-a-journey.md +82 -0
  20. package/skills/hogsend-cli/SKILL.md +1 -0
  21. package/skills/hogsend-conditions/SKILL.md +70 -0
  22. package/skills/hogsend-conditions/references/condition-types.md +251 -0
  23. package/skills/hogsend-conditions/references/durations.md +90 -0
  24. package/skills/hogsend-conditions/references/examples.md +188 -0
  25. package/skills/hogsend-database/SKILL.md +70 -0
  26. package/skills/hogsend-database/references/client-track-schema.md +97 -0
  27. package/skills/hogsend-database/references/migrations.md +132 -0
  28. package/skills/hogsend-database/references/schema-drift.md +123 -0
  29. package/skills/hogsend-deploy/SKILL.md +62 -0
  30. package/skills/hogsend-deploy/references/env-and-secrets.md +118 -0
  31. package/skills/hogsend-deploy/references/railway-two-services.md +122 -0
  32. package/skills/hogsend-deploy/references/upgrade-engine.md +92 -0
  33. package/skills/hogsend-webhooks-and-workflows/SKILL.md +68 -0
  34. package/skills/hogsend-webhooks-and-workflows/references/backfill-pattern.md +148 -0
  35. package/skills/hogsend-webhooks-and-workflows/references/custom-workflow.md +156 -0
  36. package/skills/hogsend-webhooks-and-workflows/references/webhook-source.md +172 -0
  37. package/src/commands/doctor.ts +22 -0
  38. package/src/commands/index.ts +4 -0
  39. package/src/commands/skills.ts +36 -96
  40. package/src/commands/studio.ts +261 -0
  41. package/src/commands/upgrade.ts +245 -0
  42. package/src/lib/skills.ts +186 -0
  43. package/studio/assets/index-BVA9GZqq.css +1 -0
  44. package/studio/assets/index-kPwzOOyG.js +230 -0
  45. package/studio/index.html +13 -0
@@ -0,0 +1,132 @@
1
+ # Migrations — db:generate → db:migrate
2
+
3
+ Your client track is plain Drizzle Kit. Three scripts ship in the scaffold's
4
+ `package.json`:
5
+
6
+ ```jsonc
7
+ {
8
+ "db:generate": "drizzle-kit generate", // diff schema -> ./migrations
9
+ "db:push": "drizzle-kit push", // direct sync (dev shortcut)
10
+ "db:migrate": "tsx --env-file-if-exists=.env scripts/migrate.ts" // engine THEN client
11
+ }
12
+ ```
13
+
14
+ ## drizzle.config.ts (client track)
15
+
16
+ Your `drizzle.config.ts` points Drizzle Kit at your schema, your migrations
17
+ folder, and — critically — the **client** ledger, never the engine's:
18
+
19
+ ```ts
20
+ // drizzle.config.ts
21
+ import { defineConfig } from "drizzle-kit";
22
+
23
+ export default defineConfig({
24
+ out: "./migrations",
25
+ schema: "./src/schema/index.ts",
26
+ dialect: "postgresql",
27
+ migrations: {
28
+ table: "__client_migrations", // client ledger — NOT __drizzle_migrations
29
+ schema: "drizzle",
30
+ },
31
+ dbCredentials: {
32
+ url:
33
+ process.env.DATABASE_URL ??
34
+ "postgresql://growthhog:growthhog@localhost:5434/growthhog",
35
+ },
36
+ });
37
+ ```
38
+
39
+ The `migrations.table: "__client_migrations"` line is what keeps your generated
40
+ SQL on the client track and out of the engine's `drizzle.__drizzle_migrations`
41
+ ledger.
42
+
43
+ ## The flow
44
+
45
+ ```bash
46
+ # 1. Edit src/schema/index.ts (add/change a pgTable)
47
+
48
+ # 2. Generate a client migration from the diff
49
+ pnpm db:generate
50
+ # -> writes ./migrations/NNNN_<name>.sql
51
+ # -> updates ./migrations/meta/_journal.json + a snapshot
52
+
53
+ # 3. Apply: engine track first, then your client track
54
+ pnpm db:migrate
55
+ ```
56
+
57
+ `pnpm db:migrate` runs `scripts/migrate.ts`, which calls `@hogsend/db`'s
58
+ `migrateEngine(...)` and then `migrateClient(...)`:
59
+
60
+ ```ts
61
+ // scripts/migrate.ts (scaffolded) — the shape, not for you to edit casually
62
+ import { type JournalShape, migrateClient, migrateEngine } from "@hogsend/db";
63
+
64
+ await migrateEngine(databaseUrl); // engine track first
65
+ // resolve ./migrations + read meta/_journal.json, then:
66
+ await migrateClient(databaseUrl, migrationsFolder, journal); // your client track
67
+ ```
68
+
69
+ Engine runs first so your client migrations can safely reference engine tables.
70
+ Both tracks run under a shared Postgres advisory lock, so concurrent deploys /
71
+ replicas serialize instead of racing.
72
+
73
+ ## The `./migrations` directory + meta journal
74
+
75
+ ```
76
+ migrations/
77
+ 0000_init.sql # your generated SQL
78
+ meta/
79
+ _journal.json # ordered list of your migrations (idx/tag/when)
80
+ 0000_snapshot.json # Drizzle's snapshot for the next diff
81
+ ```
82
+
83
+ `meta/_journal.json` is the source of truth for the client track. Its entries
84
+ drive which client migrations `db:migrate` applies. **Commit the whole
85
+ `migrations/` folder.** An empty journal (`{ entries: [] }`) means a trivially
86
+ in-sync client track — fine if you have no client tables yet.
87
+
88
+ ### Surfacing client drift on `/v1/health` (opt-in wiring)
89
+
90
+ The `schema.client` block on `GET /v1/health` is computed from a `clientJournal`
91
+ you pass into `createHogsendClient`. It is **opt-in** — `createHogsendClient`
92
+ defaults `clientJournal` to `{ entries: [] }`, so until you wire it the client
93
+ track always reports `inSync: true` regardless of pending client migrations.
94
+ Import your journal and thread it through in `src/index.ts`:
95
+
96
+ ```ts
97
+ // src/index.ts
98
+ import { createHogsendClient } from "@hogsend/engine";
99
+ import journal from "../migrations/meta/_journal.json" with { type: "json" };
100
+ import { buckets } from "./buckets/index.js";
101
+ import { templates } from "./emails/index.js";
102
+ import { journeys } from "./journeys/index.js";
103
+
104
+ const client = createHogsendClient({
105
+ journeys,
106
+ buckets,
107
+ email: { templates },
108
+ clientJournal: journal, // now /v1/health.schema.client reflects YOUR migrations
109
+ });
110
+ ```
111
+
112
+ `clientJournal` is the only client-track wiring; `JournalShape` (the `{ entries }`
113
+ type) is re-exported from `@hogsend/engine` if you need to annotate it. The
114
+ engine never gates boot on it — it only feeds the non-fatal `/v1/health` block.
115
+
116
+ ## `db:push` — the dev shortcut (and its trap)
117
+
118
+ `pnpm db:push` runs `drizzle-kit push`: it diffs `src/schema/index.ts` against
119
+ the live database and applies the changes **directly**, without writing a
120
+ migration file or a ledger row. Great for fast local iteration.
121
+
122
+ The trap: `db:push` leaves the **ledger behind the actual schema**. A later
123
+ `db:migrate` (or the boot guard) then sees migrations it thinks are pending even
124
+ though the objects already exist. So:
125
+
126
+ - **Local-only churn:** `db:push` is fine.
127
+ - **Anything you intend to deploy:** use `db:generate` + `db:migrate`, never
128
+ `db:push`. Deployments apply migration files; a `push`-only change won't exist
129
+ in `./migrations` and won't ship.
130
+
131
+ Recovering a database that drifted because of `db:push` is covered in
132
+ `schema-drift.md`.
@@ -0,0 +1,123 @@
1
+ # Schema drift — fatal engine track vs non-fatal client track
2
+
3
+ "Drift" = the migrations a build requires don't match what's applied to the
4
+ database. Hogsend treats the two tracks **asymmetrically**, and that asymmetry
5
+ is the whole mental model.
6
+
7
+ ## The gating policy
8
+
9
+ - **Engine track → FATAL at boot.** The running build hard-requires its bundled
10
+ engine schema. If the database is behind, the engine table you query may be
11
+ missing a column, so the app **refuses to start** rather than 500 later. A
12
+ database that is *ahead* of the build is fine (forward-compatible).
13
+ - **Client track → NON-FATAL.** You own it. You may legitimately deploy app code
14
+ ahead of an additive client migration, and a pending client migration must not
15
+ take the whole API down. It surfaces (not blocks) on `/v1/health`.
16
+
17
+ The boot guard lives in the scaffold's `src/index.ts` and checks only the engine
18
+ track:
19
+
20
+ ```ts
21
+ // src/index.ts (scaffolded boot guard)
22
+ import { getEngineSchemaVersion } from "@hogsend/engine";
23
+
24
+ if (process.env.SKIP_SCHEMA_CHECK !== "true") {
25
+ const schema = await getEngineSchemaVersion(client.db);
26
+ if (!schema.inSync) {
27
+ client.logger.error(
28
+ `Database schema is out of date: this build requires ${schema.required}, ` +
29
+ `database is at ${schema.applied ?? "(empty)"}. ` +
30
+ `Pending migration(s): ${schema.pending.join(", ") || "(unknown — is the DB reachable?)"}. ` +
31
+ "Run `pnpm db:migrate`, or set SKIP_SCHEMA_CHECK=true to bypass.",
32
+ );
33
+ await client.dbClient.end({ timeout: 5 });
34
+ process.exit(1);
35
+ }
36
+ client.logger.info(`Database schema in sync at ${schema.applied}`);
37
+ }
38
+ ```
39
+
40
+ Note it calls `getEngineSchemaVersion` (re-exported by `@hogsend/engine`) — the
41
+ **client** track is deliberately NOT in the boot guard. `client.db` is the
42
+ container's Drizzle instance; `client.dbClient` is the underlying postgres-js
43
+ connection it closes before exiting.
44
+
45
+ ## Reading drift off `GET /v1/health`
46
+
47
+ `/v1/health` reports both tracks. The top-level `status` is `migration_pending`
48
+ if **either** track is behind:
49
+
50
+ ```jsonc
51
+ {
52
+ "status": "migration_pending", // healthy | degraded | migration_pending
53
+ "schema": {
54
+ "engine": { "applied": "0042_…", "required": "0042_…", "inSync": true, "pending": [] },
55
+ "client": { "applied": "0000_init", "required": "0001_add_tickets",
56
+ "inSync": false, "pending": ["0001_add_tickets"] }
57
+ },
58
+ "components": { "database": { "status": "up" }, "redis": { "status": "up" } }
59
+ }
60
+ ```
61
+
62
+ - `schema.engine.inSync: false` should be impossible on a *running* app — the
63
+ boot guard would have exited. If you ever see it, the app was started with
64
+ `SKIP_SCHEMA_CHECK=true`.
65
+ - `schema.client.inSync: false` is the normal "I haven't run `db:migrate` yet"
66
+ signal — non-fatal, but your responsibility to clear. **Note:** the client
67
+ block only reflects your migrations if you wired `clientJournal` into
68
+ `createHogsendClient` (it defaults to `{ entries: [] }` ⇒ always `inSync: true`).
69
+ See the `clientJournal` opt-in wiring in `migrations.md`.
70
+
71
+ `status` decoding:
72
+
73
+ | `status` | meaning |
74
+ |---|---|
75
+ | `healthy` | both tracks `inSync`, db + redis up |
76
+ | `degraded` | both tracks `inSync`, but db or redis is down |
77
+ | `migration_pending` | engine OR client track is behind |
78
+
79
+ To check this from the CLI (any instance, local or prod) without parsing JSON by
80
+ hand, use `hogsend doctor --json` — see the **hogsend-cli** skill. It hits the
81
+ unauthenticated `/v1/health` and gives a drift verdict.
82
+
83
+ ## Fixing drift
84
+
85
+ - **Client track behind** (the common case): `pnpm db:migrate`. If you changed
86
+ `src/schema/` but never generated, run `pnpm db:generate` first.
87
+ - **Engine track behind** (app won't boot after a `@hogsend/*` bump): `pnpm
88
+ db:migrate` applies the new engine migrations that arrived with the bump, then
89
+ the boot guard passes.
90
+
91
+ ## `SKIP_SCHEMA_CHECK` — emergency bypass only
92
+
93
+ `SKIP_SCHEMA_CHECK=true` makes `src/index.ts` skip the engine boot guard so the
94
+ app starts even on a behind-engine database. Use it only to bring an instance up
95
+ during an incident; it does NOT fix the schema, and the first query against a
96
+ missing column will still fail. Clear the drift with `db:migrate` and remove the
97
+ flag.
98
+
99
+ ## The `db:push` ledger trap (and recovery)
100
+
101
+ A database bootstrapped with `pnpm db:push` has the schema objects but **no
102
+ ledger rows** — so `db:migrate` and the boot guard think migrations are pending
103
+ even though every object already exists, and you'll see `migration_pending` /
104
+ "type already exists" errors.
105
+
106
+ - **Client track:** since `db:push` is for throwaway local churn, the cleanest
107
+ fix is to reset that dev database and run `pnpm db:migrate` from clean, so the
108
+ ledger and schema agree. For anything real, generate + migrate from the start.
109
+ - **Engine track:** `@hogsend/db` ships a `db:stamp` recovery tool that records
110
+ every bundled engine migration as *applied* WITHOUT running SQL — for the
111
+ exact case where a dev DB was `db:push`-ed and the engine ledger is behind a
112
+ schema that already matches HEAD. It is an engine-package script (run inside
113
+ `@hogsend/db`), not a consumer command, and it is dangerous if the schema is
114
+ genuinely missing objects (it would mark migrations applied without creating
115
+ them). Reach for a clean `db:migrate` first; only stamp when you are certain
116
+ the schema already matches HEAD.
117
+
118
+ ## Bottom line
119
+
120
+ You can only cause **client-track** drift (it's the only track you author), and
121
+ client-track drift never takes the app down — it just lights up
122
+ `migration_pending` on `/v1/health` until you `pnpm db:migrate`. Engine-track
123
+ drift is the engine's job to gate, and it does so fatally at boot.
@@ -0,0 +1,62 @@
1
+ ---
2
+ name: hogsend-deploy
3
+ description: Use when deploying or configuring production infra for YOUR scaffolded Hogsend app — Railway two services (api via railway.toml with /v1/health + pre-deploy db:migrate; worker via railway.worker.toml, no healthcheck), Hatchet-Lite, required vs optional env, and upgrading the engine + refreshing vendored skills. This is for shipping your own app, NOT the maintainer's package-release process.
4
+ license: MIT
5
+ metadata:
6
+ author: withSeismic
7
+ version: "1.0.0"
8
+ ---
9
+
10
+ # Hogsend Deploy
11
+
12
+ This skill helps you ship and operate YOUR scaffolded Hogsend app in
13
+ production: standing up the two Railway services that ship in your repo, wiring
14
+ the env Hogsend needs at boot, pointing at a Hatchet-Lite engine, and keeping
15
+ your `@hogsend/*` deps + vendored agent skills on the latest line.
16
+
17
+ This is the **consumer** deploy guide — for shipping the app you scaffolded with
18
+ `create-hogsend`. It is NOT the maintainer's npm release / version-line process
19
+ (that lives in the separate `release` skill).
20
+
21
+ > These are side-effecting production operations — provisioning services,
22
+ > writing secrets, running migrations against a real database, bumping deps.
23
+ > Run each step deliberately and confirm intent before executing.
24
+
25
+ ## Key concepts
26
+
27
+ - **Two services, one repo.** Your scaffold ships `railway.toml` (the HTTP
28
+ **api**) and `railway.worker.toml` (the Hatchet **worker**). Same codebase,
29
+ two Railway services with different config files.
30
+ - **api owns migrations + healthcheck.** The api's `preDeployCommand` runs
31
+ `pnpm db:migrate` (two-track: engine then client) before boot; it exposes
32
+ `/v1/health`. The worker has no HTTP port, no healthcheck, and never migrates.
33
+ - **Hatchet-Lite is your orchestration engine.** The worker connects to it over
34
+ gRPC with `HATCHET_CLIENT_TOKEN` + `HATCHET_CLIENT_HOST_PORT`. Locally it runs
35
+ via `docker-compose.yml`; in prod it's its own service.
36
+ - **Three required env vars, the rest optional.** `DATABASE_URL`,
37
+ `BETTER_AUTH_SECRET`, `RESEND_API_KEY` are hard-required at boot; PostHog,
38
+ webhook secrets, and the admin key are opt-in.
39
+ - **Upgrades move code + agent guidance together.** `hogsend upgrade` bumps
40
+ `@hogsend/*` AND refreshes the vendored `.claude/skills`; `hogsend doctor`
41
+ nudges you when those skills fall behind.
42
+
43
+ ## Task playbooks — load the matching reference
44
+
45
+ - **Stand up / configure the two Railway services (api + worker), healthcheck,
46
+ pre-deploy migrate, Hatchet-Lite** → `references/railway-two-services.md`
47
+ - **Decide which env vars are required vs optional and where each comes from** →
48
+ `references/env-and-secrets.md`
49
+ - **Bump the engine after a release + refresh vendored skills (`hogsend upgrade`
50
+ / `--skills-only` / `skills add --all --force`, the `doctor` staleness nudge)**
51
+ → `references/upgrade-engine.md`
52
+
53
+ ## Golden rules
54
+
55
+ 1. Migrate exactly once per deploy: only the **api** service runs
56
+ `db:migrate` (its `preDeployCommand`). Never add a migrate step to the worker.
57
+ 2. Set the three required secrets BEFORE the first deploy — the app hard-fails
58
+ at boot without them, and the api healthcheck will never go green.
59
+ 3. After a deploy, verify with `hogsend doctor --url <prod> --json` and expect
60
+ an `ok` verdict with the schema in sync (see the hogsend-cli skill).
61
+ 4. After an engine bump, refresh the vendored skills in the same step so the
62
+ agent guidance matches the code you're now running.
@@ -0,0 +1,118 @@
1
+ # Env & secrets
2
+
3
+ Your scaffold ships a `.env.example` documenting every variable Hogsend reads.
4
+ The engine validates env at startup (`@t3-oss/env-core`), so the three
5
+ **required** vars must be present before the app will boot — otherwise the api
6
+ crashes and its `/v1/health` healthcheck never goes green.
7
+
8
+ This page is the prod-deploy view of that file: what's required, what's
9
+ optional, and which service needs each.
10
+
11
+ ## Required at boot
12
+
13
+ These three are hard-required everywhere (local and prod). Set them on **both**
14
+ the api and worker services in Railway.
15
+
16
+ ```bash
17
+ # Postgres connection string (TimescaleDB in prod).
18
+ DATABASE_URL=postgresql://user:pass@host:5432/dbname
19
+
20
+ # Better Auth signing secret — MINIMUM 32 characters. Generate a real random
21
+ # value (the local bootstrap/setup flow mints one for you; never reuse the
22
+ # placeholder from .env.example).
23
+ BETTER_AUTH_SECRET=<random, >=32 chars>
24
+
25
+ # Resend API key — required for any email send.
26
+ RESEND_API_KEY=re_...
27
+ ```
28
+
29
+ If any of these is missing or invalid, env validation throws at startup. On the
30
+ api that means the healthcheck never passes; on the worker it means the process
31
+ exits and Railway restart-loops it.
32
+
33
+ ## Core operational vars
34
+
35
+ Not "secrets," but you'll want to set these explicitly in prod:
36
+
37
+ ```bash
38
+ NODE_ENV=production # disables /docs + /openapi.json
39
+ PORT=3002 # api HTTP port (Railway may inject its own)
40
+ LOG_LEVEL=info
41
+
42
+ REDIS_URL=redis://host:6379 # PostHog property cache
43
+
44
+ # Public base URL of the deployed api — used to build unsubscribe + tracking
45
+ # links inside outgoing emails, and as the tracking domain for click/open
46
+ # rewriting. Set this to your real api domain or links will point at localhost.
47
+ API_PUBLIC_URL=https://api.yourapp.com
48
+ BETTER_AUTH_URL=https://api.yourapp.com
49
+
50
+ # Default From address for sends.
51
+ RESEND_FROM_EMAIL=noreply@yourapp.com
52
+
53
+ # Which journeys load. "*" = all, or a comma-separated list of journey IDs.
54
+ ENABLED_JOURNEYS=*
55
+ ```
56
+
57
+ ## Hatchet (worker + api)
58
+
59
+ Both processes talk to your Hatchet-Lite service. Mint the token from the
60
+ Hatchet dashboard and set these on the api AND the worker:
61
+
62
+ ```bash
63
+ HATCHET_CLIENT_TOKEN=<token minted from the Hatchet dashboard>
64
+ HATCHET_CLIENT_HOST_PORT=hatchet-host:7077
65
+ HATCHET_CLIENT_TLS_STRATEGY=none # or tls, per your Hatchet deployment
66
+ ```
67
+
68
+ (Locally these point at the docker-compose Hatchet-Lite: `localhost:7077`,
69
+ dashboard at `http://localhost:8888`, login `admin@example.com` / `Admin123!!`.)
70
+
71
+ ## Optional
72
+
73
+ All commented-out in `.env.example` — add only what you use:
74
+
75
+ ```bash
76
+ # PostHog person properties + event capture (no-op if unset).
77
+ POSTHOG_API_KEY=phc_...
78
+ POSTHOG_HOST=https://us.i.posthog.com
79
+
80
+ # Verify incoming PostHog webhooks (POST /v1/webhooks/posthog).
81
+ POSTHOG_WEBHOOK_SECRET=...
82
+
83
+ # Verify Resend bounce/complaint webhooks.
84
+ RESEND_WEBHOOK_SECRET=whsec_...
85
+
86
+ # Required for the /v1/admin/* routes + CLI access. Without it, admin reads
87
+ # (stats, contacts, journeys via the CLI) are unavailable in prod.
88
+ ADMIN_API_KEY=...
89
+ ```
90
+
91
+ Notes:
92
+
93
+ - **PostHog is fully optional.** Without `POSTHOG_API_KEY`, person-property
94
+ fetches and event captures are no-ops — journeys still run.
95
+ - **Webhook secrets are per-source.** Only set the secret for a webhook source
96
+ you've actually registered (see the consumer's `src/webhook-sources`).
97
+ - **`ADMIN_API_KEY` gates `/v1/admin/*`.** Set it in prod if you want to drive
98
+ the deployed instance with the `hogsend` CLI (`stats`, `contacts`,
99
+ `journeys`). `hogsend doctor` hits the unauthenticated `/v1/health` and needs
100
+ no key. See the hogsend-cli skill for how the CLI resolves the key.
101
+
102
+ ## Service-by-service checklist
103
+
104
+ | Var | api | worker |
105
+ |-----|-----|--------|
106
+ | `DATABASE_URL` | required | required |
107
+ | `BETTER_AUTH_SECRET` | required | required |
108
+ | `RESEND_API_KEY` | required | required |
109
+ | `REDIS_URL` | yes | yes |
110
+ | `API_PUBLIC_URL` / `BETTER_AUTH_URL` | yes | — |
111
+ | `HATCHET_CLIENT_TOKEN` / `_HOST_PORT` / `_TLS_STRATEGY` | yes | yes |
112
+ | `ENABLED_JOURNEYS` | yes | yes |
113
+ | `POSTHOG_*` (optional) | if used | if used |
114
+ | `*_WEBHOOK_SECRET` (optional) | if used | — |
115
+ | `ADMIN_API_KEY` (optional) | if used | — |
116
+
117
+ Set the shared/required vars on both services; the worker doesn't serve HTTP so
118
+ it doesn't need the public-URL or webhook/admin vars.
@@ -0,0 +1,122 @@
1
+ # Railway: two services from one repo
2
+
3
+ Your scaffolded app ships TWO Railway config files at the repo root. Deploy them
4
+ as **two separate Railway services pointed at the same GitHub repo** — they
5
+ share the codebase but run different processes with different config.
6
+
7
+ | Service | Config file | Process | Healthcheck | Migrations |
8
+ |---------|-------------|---------|-------------|------------|
9
+ | api | `railway.toml` | HTTP API (`pnpm start`) | `/v1/health` | runs `db:migrate` (pre-deploy) |
10
+ | worker | `railway.worker.toml` | Hatchet worker (`pnpm worker`) | none (no HTTP port) | never |
11
+
12
+ Both are driven by `git push` — pushing to your deploy branch triggers a build
13
+ for whichever services watch the changed paths.
14
+
15
+ ## The api service (`railway.toml`)
16
+
17
+ ```toml
18
+ [build]
19
+ buildCommand = "pnpm build"
20
+ watchPatterns = ["src/**", "migrations/**", "package.json", "pnpm-lock.yaml", "railway.toml"]
21
+
22
+ [deploy]
23
+ # Two-track migrate: engine track first, then this repo's client track.
24
+ # scripts/migrate.ts runs both in order and skips an empty client track
25
+ # gracefully. Engine MUST succeed before the API boots (boot guard in
26
+ # src/index.ts hard-requires the engine schema).
27
+ preDeployCommand = "pnpm db:migrate"
28
+ startCommand = "pnpm start"
29
+ healthcheckPath = "/v1/health"
30
+ healthcheckTimeout = 120
31
+ restartPolicyType = "ON_FAILURE"
32
+ restartPolicyMaxRetries = 3
33
+ ```
34
+
35
+ Key points:
36
+
37
+ - **`preDeployCommand = "pnpm db:migrate"`** runs BEFORE the new container takes
38
+ traffic. It is two-track: the engine schema migrates first (the api's boot
39
+ guard in `src/index.ts` hard-requires it), then your client track. An empty
40
+ client track is skipped gracefully, so this works whether or not you've added
41
+ your own migrations under `src/schema`.
42
+ - **`healthcheckPath = "/v1/health"`** — Railway holds traffic until this route
43
+ returns healthy. The route reports component status (database, redis) and the
44
+ two-track schema state, which is exactly what `hogsend doctor` reads.
45
+ `healthcheckTimeout = 120` gives migrations + boot time to settle.
46
+ - **Restart policy** retries on failure up to 3 times.
47
+
48
+ When this service is healthy, point your public domain (e.g.
49
+ `api.yourapp.com` via a CNAME) at it.
50
+
51
+ ## The worker service (`railway.worker.toml`)
52
+
53
+ ```toml
54
+ [build]
55
+ buildCommand = "pnpm build"
56
+ watchPatterns = ["src/**", "package.json", "pnpm-lock.yaml", "railway.worker.toml"]
57
+
58
+ [deploy]
59
+ # No healthcheck — the worker has no HTTP port. Migrations are owned by the API
60
+ # service's preDeployCommand; the worker just executes Hatchet tasks.
61
+ startCommand = "pnpm worker"
62
+ restartPolicyType = "ON_FAILURE"
63
+ restartPolicyMaxRetries = 5
64
+ ```
65
+
66
+ Key points:
67
+
68
+ - **No `healthcheckPath`** — the worker is a long-running process with no HTTP
69
+ port, so there's nothing to probe. Don't add one; a healthcheck that can never
70
+ pass would keep the deploy from going green.
71
+ - **No `preDeployCommand`** — migrations are owned solely by the api service.
72
+ The worker just connects to Hatchet and executes tasks (`sendEmailTask`,
73
+ `importContactsTask`, `checkAlertsTask`, plus your enabled journey tasks and
74
+ any `extraWorkflows`).
75
+ - **`startCommand = "pnpm worker"`** runs the production worker entry
76
+ (`src/worker.ts` → `createWorker({ container, journeys })`).
77
+ - **It scales independently** of the api — add worker replicas to process more
78
+ Hatchet tasks in parallel without touching the api.
79
+
80
+ ### Pointing Railway at the worker config
81
+
82
+ A Railway service builds from a single config file. To make the second service
83
+ use `railway.worker.toml` instead of the default `railway.toml`, set the service
84
+ config-file path in the Railway dashboard (Service → Settings → Config-as-code /
85
+ Railway config file) to `railway.worker.toml`.
86
+
87
+ ## Hatchet-Lite
88
+
89
+ Hatchet orchestrates durable task execution (email sends, journey runs,
90
+ background jobs). The api process pushes events to it; the worker process
91
+ executes the tasks it routes.
92
+
93
+ - **Locally** it runs via `docker-compose.yml` (started by `pnpm bootstrap` or
94
+ `hogsend setup`): dashboard on `:8888`, gRPC on `:7077`, with its own
95
+ Postgres 15. Default dashboard login: `admin@example.com` / `Admin123!!`.
96
+ - **In production** Hatchet-Lite is its own Railway service (the
97
+ `ghcr.io/hatchet-dev/hatchet/hatchet-lite:latest` image). Both the api and the
98
+ worker connect to it via `HATCHET_CLIENT_TOKEN` + `HATCHET_CLIENT_HOST_PORT`
99
+ (and `HATCHET_CLIENT_TLS_STRATEGY`). Mint the token from the Hatchet
100
+ dashboard.
101
+
102
+ See `references/env-and-secrets.md` for the exact Hatchet env keys and which
103
+ services need them.
104
+
105
+ ## Deploy → verify loop
106
+
107
+ 1. Set the required secrets on **both** services first (see
108
+ `references/env-and-secrets.md`). The api won't pass its healthcheck without
109
+ `DATABASE_URL` / `BETTER_AUTH_SECRET` / `RESEND_API_KEY`.
110
+ 2. Push to your deploy branch. Railway builds the api (runs `pnpm db:migrate`
111
+ pre-deploy) and the worker.
112
+ 3. Verify the live api with the CLI:
113
+
114
+ ```bash
115
+ hogsend doctor --url https://api.yourapp.com --json
116
+ ```
117
+
118
+ Expect an `ok` verdict with `database`/`redis` up and the engine + client
119
+ schema in sync. A `migration_pending` verdict means the pre-deploy migrate
120
+ didn't catch up — re-check the api's pre-deploy logs. `unreachable` means the
121
+ healthcheck never went green (often a missing required secret). See the
122
+ hogsend-cli skill for the full doctor playbook.
@@ -0,0 +1,92 @@
1
+ # Upgrade the engine + refresh vendored skills
2
+
3
+ Hogsend is a versioned engine you consume as a dependency. After a new engine
4
+ release you want your app's `@hogsend/*` deps AND the vendored Claude Code
5
+ skills under `.claude/skills` to move together — so the code you run and the
6
+ agent guidance that drives it stay in lockstep.
7
+
8
+ > This is a side-effecting operation: it bumps dependencies and rewrites files
9
+ > in `.claude/skills`. Run it deliberately, review the diff, and re-test before
10
+ > deploying.
11
+
12
+ ## One step: `hogsend upgrade`
13
+
14
+ ```bash
15
+ hogsend upgrade
16
+ ```
17
+
18
+ This does both halves in order:
19
+
20
+ 1. **Bumps every `@hogsend/*` dependency** declared in your `package.json`
21
+ (`dependencies` + `devDependencies`) to `latest`, using the package manager
22
+ detected from your lockfile.
23
+ 2. **Refreshes the vendored skills** in `./.claude/skills` to match, then
24
+ version-stamps them so `hogsend doctor` can later tell when they fall behind.
25
+
26
+ If the dependency bump hard-fails, the skills refresh is skipped (fix the bump,
27
+ then re-run) — the two never drift apart silently.
28
+
29
+ ### Useful flags
30
+
31
+ ```bash
32
+ hogsend upgrade --to 1.4.0 # pin a specific target instead of latest
33
+ hogsend upgrade --pm pnpm # force a package manager (default: from lockfile)
34
+ hogsend upgrade --cwd ./apps/x # run against a different project root
35
+ hogsend upgrade --deps-only # bump deps only; leave skills untouched
36
+ hogsend upgrade --skills-only # refresh skills only; don't touch deps
37
+ hogsend upgrade --yes # skip the confirmation prompt
38
+ hogsend upgrade --json # non-interactive, single JSON result (implies --yes)
39
+ ```
40
+
41
+ `--deps-only` and `--skills-only` are mutually exclusive.
42
+
43
+ ## Refresh skills on their own
44
+
45
+ If you only need to re-vendor the bundled skills (e.g. you bumped the engine by
46
+ hand, or want the latest guidance without changing versions), use either:
47
+
48
+ ```bash
49
+ hogsend upgrade --skills-only # refresh + re-stamp via upgrade
50
+ hogsend skills add --all --force # copy every bundled skill, overwriting
51
+ ```
52
+
53
+ `hogsend skills add --all --force` copies all bundled skills into
54
+ `./.claude/skills/<name>/`, overwriting existing copies (`--force` is what makes
55
+ it overwrite rather than skip), and re-stamps the installed set. Without
56
+ `--force`, already-installed skills are skipped. You can also target one skill:
57
+
58
+ ```bash
59
+ hogsend skills list # see what's bundled + what's installed
60
+ hogsend skills add hogsend-cli --force
61
+ ```
62
+
63
+ ## The `hogsend doctor` staleness nudge
64
+
65
+ `hogsend doctor` (the health probe; see the hogsend-cli skill) does a
66
+ best-effort check: if your vendored skills were installed by an OLDER CLI than
67
+ the one now running, it prints a nudge:
68
+
69
+ ```
70
+ Skills out of date
71
+ Vendored Claude skills are from v1.2.0; this CLI is v1.4.0.
72
+ Refresh: hogsend upgrade (deps + skills) or hogsend skills add --all --force.
73
+ ```
74
+
75
+ This is silent when there's no stamp (not a tracked app dir) and suppressed in
76
+ `--json` mode (the staleness verdict is still surfaced under the `skills` key of
77
+ the JSON output instead). Treat the nudge as your signal to run `hogsend
78
+ upgrade`.
79
+
80
+ ## After upgrading
81
+
82
+ 1. Review the dependency + `.claude/skills` diff.
83
+ 2. Re-run your type-check / tests against the new engine line.
84
+ 3. Re-deploy (push to your deploy branch — see
85
+ `references/railway-two-services.md`). The api's pre-deploy `db:migrate`
86
+ applies any new engine migrations that came with the bump.
87
+ 4. Verify the live instance with `hogsend doctor --url <prod> --json` and
88
+ confirm the schema is in sync.
89
+
90
+ > This is the **consumer** upgrade flow for your own app. It is NOT the
91
+ > maintainer's npm release / version-line process for publishing `@hogsend/*` —
92
+ > that's the separate `release` skill.