@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.
- package/dist/bin.js +575 -104
- package/dist/bin.js.map +1 -1
- package/package.json +4 -1
- package/skills/hogsend-authoring-buckets/SKILL.md +69 -0
- package/skills/hogsend-authoring-buckets/references/bucket-id-aliases.md +117 -0
- package/skills/hogsend-authoring-buckets/references/bucket-meta.md +142 -0
- package/skills/hogsend-authoring-buckets/references/buckets-vs-journeys.md +96 -0
- package/skills/hogsend-authoring-buckets/references/register-a-bucket.md +129 -0
- package/skills/hogsend-authoring-emails/SKILL.md +68 -0
- package/skills/hogsend-authoring-emails/references/email-components.md +112 -0
- package/skills/hogsend-authoring-emails/references/preview-and-render.md +116 -0
- package/skills/hogsend-authoring-emails/references/template-four-file-contract.md +134 -0
- package/skills/hogsend-authoring-emails/references/tracking-and-unsubscribe.md +127 -0
- package/skills/hogsend-authoring-journeys/SKILL.md +88 -0
- package/skills/hogsend-authoring-journeys/references/branch-on-engagement.md +117 -0
- package/skills/hogsend-authoring-journeys/references/journey-context.md +133 -0
- package/skills/hogsend-authoring-journeys/references/journey-meta.md +145 -0
- package/skills/hogsend-authoring-journeys/references/register-a-journey.md +99 -0
- package/skills/hogsend-authoring-journeys/references/sending-email-from-a-journey.md +82 -0
- package/skills/hogsend-cli/SKILL.md +1 -0
- package/skills/hogsend-conditions/SKILL.md +70 -0
- package/skills/hogsend-conditions/references/condition-types.md +251 -0
- package/skills/hogsend-conditions/references/durations.md +90 -0
- package/skills/hogsend-conditions/references/examples.md +188 -0
- package/skills/hogsend-database/SKILL.md +70 -0
- package/skills/hogsend-database/references/client-track-schema.md +97 -0
- package/skills/hogsend-database/references/migrations.md +132 -0
- package/skills/hogsend-database/references/schema-drift.md +123 -0
- package/skills/hogsend-deploy/SKILL.md +62 -0
- package/skills/hogsend-deploy/references/env-and-secrets.md +118 -0
- package/skills/hogsend-deploy/references/railway-two-services.md +122 -0
- package/skills/hogsend-deploy/references/upgrade-engine.md +92 -0
- package/skills/hogsend-webhooks-and-workflows/SKILL.md +68 -0
- package/skills/hogsend-webhooks-and-workflows/references/backfill-pattern.md +148 -0
- package/skills/hogsend-webhooks-and-workflows/references/custom-workflow.md +156 -0
- package/skills/hogsend-webhooks-and-workflows/references/webhook-source.md +172 -0
- package/src/commands/doctor.ts +22 -0
- package/src/commands/index.ts +4 -0
- package/src/commands/skills.ts +36 -96
- package/src/commands/studio.ts +261 -0
- package/src/commands/upgrade.ts +245 -0
- package/src/lib/skills.ts +186 -0
- package/studio/assets/index-BVA9GZqq.css +1 -0
- package/studio/assets/index-kPwzOOyG.js +230 -0
- 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.
|