IAMScouting docs

IAMScouting — environment setup

Reference for production env vars and what happens when each is missing or

mis-configured. Maintained alongside /lib/logger.ts so the fail-modes here

match what actually ends up in app_error_log / the fallback file.

Required env vars

The application boots without every var below, but routes that depend on a

missing var return a 503 service_unavailable and log to app_error_log

(falling back to /var/log/iams-audit-fail.log if the DB is unavailable).

| Variable | Used by | Failure mode |

| --- | --- | --- |

| DATABASE_URL / SUPABASE_* | All DB access | App refuses to boot |

| STRIPE_SECRET_KEY | /api/network/leads/[id]/boost, billing portal | 503 + server log |

| ANTHROPIC_API_KEY | /api/network/ai-assistant | 503 + server log |

| RESEND_API_KEY | Transactional email | Mails are dropped silently |

| ADMIN_ALLOWLIST | /api/admin/* + admin pages | Defaults to start@iamscouting.com |

stripe-keys

Lead Boost (the only paid one-off on /network) creates a Stripe Checkout

session in /app/api/network/leads/[id]/boost/route.ts. The route uses

requireStripe() from /lib/stripe.ts, which throws when

STRIPE_SECRET_KEY is blank.

Operational note (2026-05-21): the VPS env file was observed with a

blank STRIPE_SECRET_KEY=. The route now returns a 503 with a generic

"Boost service temporarily unavailable" instead of leaking the raw error.

To restore the feature:

  1. Pull the live secret from the Stripe dashboard (Test or Live keys —

matching whichever STRIPE_MODE is set on the box).

  1. Write it into /opt/iamscouting/.env as STRIPE_SECRET_KEY=sk_live_….
  2. Restart the container: docker compose up -d --force-recreate.
  3. Verify with `curl -sX POST .../api/network/leads/<id>/boost -d

'{"duration":"14d"}' — expect a checkout_url`, not a 503.

The STRIPE_WEBHOOK_SECRET is a separate env var required for the

/api/stripe/webhook endpoint; verify both are set together.

Annual Stripe price IDs (v0.8)

/api/network/subscribe resolves the Stripe price_id for each

(tier_code, interval) pair in this order:

  1. Row in stripe_prices with interval='year' and active=true
  2. annual_price_id column on the matching monthly row (added in

migration 0065_stripe_annual_coupons.sql)

  1. Env var STRIPE_PRICE_<TIER>_ANNUAL — one of:

- STRIPE_PRICE_STARTER_ANNUAL

- STRIPE_PRICE_PRO_ANNUAL

- STRIPE_PRICE_ENTERPRISE_ANNUAL

  1. Falls back to the monthly price so the upgrade flow never hard-fails

(UI shows annual price label but Stripe charges monthly until the

annual SKU is wired). Logged via `event_log.price_source =

'monthly_fallback'`.

Place real price_xxx IDs from the Stripe dashboard into the env file

on the VPS and restart the container. No code redeploy required.

Coupon codes (v0.8)

The coupon_codes table (migration 0065) maps a user-facing promo

string (e.g. LAUNCH50) to the corresponding Stripe promotion_code_id

(promo_xxx). The /account/club-tier page validates codes against

/api/coupon/validate before forwarding to /api/network/subscribe,

which attaches the promo to the Checkout session. The webhook

increments used_count on checkout.session.completed. To seed:

INSERT INTO coupon_codes (code, stripe_promotion_code_id, percent_off, max_uses, valid_until)
VALUES ('LAUNCH50', 'promo_xxx', 50, 100, NOW() + INTERVAL '30 days');

Codes without a stripe_promotion_code_id still validate (useful for

soft launch testing) but won't change the price at Checkout.

anthropic-key

/api/network/ai-assistant uses the Anthropic SDK with model

claude-opus-4-7 (or whatever ANTHROPIC_MODEL_RECOMMENDER overrides).

The route now wraps every upstream call: missing key → 503, SDK throw →

503, empty draft → 503. The real error message is in

app_error_log server-side.

Operational note (2026-05-21): the production key has a $0 credit

balance and is returning HTTP 402 from Anthropic. From the client's

perspective this surfaces as a 503 "AI service temporarily unavailable" —

the same shape as a missing-key error, by design (don't leak billing

state).

To restore:

  1. Top up the workspace at https://console.anthropic.com/settings/billing.
  2. Confirm a 200 response from `curl

https://api.anthropic.com/v1/messages -H "x-api-key: $KEY" -d

'{"model":"claude-opus-4-7","max_tokens":8,"messages":[{"role":"user","content":"ping"}]}'`.

Resend / welcome email

(auth)/actions.ts fires a welcome email (welcomeEmail() template in

lib/email/templates.ts) immediately after every successful signup. The

send is best-effort — failures are logged but do not block signup.

Sender selection:

IAMScouting <no-reply@iamscouting.com>). Set this only after the

iamscouting.com domain has been verified in the Resend dashboard

(SPF + DKIM TXT records added at the registrar).

IAMScouting <onboarding@resend.dev>. Resend allows any API key to

send from this sender, so the welcome email still arrives during

domain verification. Reply-to is always start@iamscouting.com.

To enable a verified domain:

  1. In the Resend dashboard, add iamscouting.com as a domain.
  2. Copy the SPF + DKIM records into the registrar's DNS panel.
  3. Wait for "Verified" status, then set RESEND_VERIFIED_DOMAIN=true in

/opt/iamscouting/.env and restart the container.

Beta invite gate

Soft-launch signup throttle (migration 0069_beta_invites.sql):

unredeemed code (the ?invite=… query param or hidden form field).

still recorded as redeemed if supplied, so the admin queue at

/admin/beta-invites stays accurate.

Mint codes from /admin/beta-invites. The "Copy invite link" button

yields https://iamscouting.com/signup?invite=CODE ready to paste into

an outreach email.

Audit log fallback

When the Postgres insert into app_error_log or admin_audit_log fails,

the logger now writes a JSON line to the path in

IAMS_FALLBACK_LOG_PATH (defaults to /var/log/iams-audit-fail.log).

This file is included in the nightly backup tarball — see

scripts/backup-daily.sh.