feat(cloud): email magic-link sign-in, and a real public landing page - #413
Merged
Conversation
GitHub and Google both require an account with that provider; email magic-link removes that barrier entirely. Wires next-auth's Resend provider, gated the same way as GitHub/Google (env vars present = offered). The Email provider is the first to need a database adapter (Auth.js stores its one-time tokens there), so this also wires PrismaAdapter in — but only when Resend is configured, so a deployment without it is unchanged. GitHub and Google get allowDangerousEmailAccountLinking so the adapter's account-linking check doesn't newly reject a user who signed up with one provider and later tries another with the same email — ensureUserOrg has always merged by email with no such check. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
The root page redirected unconditionally to /signin, so the site's only public face was a bare auth form with no context for what it was gating — indistinguishable from an unfinished stub. Signed-out visitors now see what Ghost does with an explicit sign-in link; signed-in visitors still land straight on /dashboard. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
|
You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard. |
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
…andoff The Chrome extension merged to master in PR #409; this doc still said "not yet merged." Also adds a "Make Ghost demo-able" section: the deploy/auth/domain gaps are closed, but nothing on the live site shows the trust loop running — the fixtures/order workflow works but isn't visible or walkable from the public site. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Dashboard previously showed a static "what Ghost does" list duplicated from the landing page, with no link anywhere in the app — a dead end for a signed-in user with no workflows. It now points a workflow-less org straight at creating the demo workflow, and an org with workflows gets quick links to Workflows/Runs/Audit. Workflows and Recordings list rows were missing flex-1 on the name column, so it collapsed to near-zero width next to the shrink-0 button cluster and wrapped text letter-by-letter (visible live on the Workflows page with the demo workflow's long name).
DEPLOYMENT.md and deploy-website.yml both claimed ghost.muharafiq.com serves the static public/ marketing site. It doesn't — the ghost-app Vercel project's Root Directory was repointed at cloud/apps/web, which now serves the live cloud SaaS app there directly. Since that override applies regardless of what changed in a push, deploy-website.yml would silently redeploy the wrong app (cloud/apps/web, not public/) if triggered, despite its own logging claiming otherwise. Disabled its push trigger and added warnings until public/ gets its own Vercel target or is retired; updated DEPLOYMENT.md and CLAUDE.md to describe what's actually live.
flex-1 sets flex-basis:0%, which the flex-wrap algorithm treats as zero hypothetical size — so it never triggers a wrap, it just keeps shrinking min-w-0 content down to a word-per-line column. Confirmed live: the Workflows header and its demo row still squeezed at full browser width even after the earlier min-w-0/flex-1 patch, with the title/name partially hidden behind the button cluster. Switched to the standard pattern instead: flex-wrap on the row, no forced shrink on the title/name block, so the button/control cluster drops to its own line once it doesn't fit, rather than crushing the text next to it. Already deployed to production; this commit just catches git history up to what's live.
Ghost's engine worked; getting it running did not. Three defects made a
correctly-built stack look dead, and all three were invisible — no error,
just a UI that renders and never does anything.
1. Nothing loaded `cloud/.env`. The README has always said `cp .env.example
.env && pnpm dev`, but that file sits at the workspace root while the apps
run from `apps/web` and `apps/worker`. Next reads `.env` relative to its own
project directory; the worker read nothing and died on "REDIS_URL is not
set". Every local setup that appeared to work was one where someone had
exported the variables by hand, which is why it never reproduced for them.
`packages/core/src/env.ts` now loads it for both, without overriding a real
environment (so Vercel and containers are unaffected).
2. `demo.sh` treated an open port as a working service. `port_open 5432` is
true for any Postgres — Homebrew's, Postgres.app's, another project's — and
each accepts the connection, denies the `ghost` user, and turns every query
into an unexplained 500 later. Setup reported success, so the step that
caused the failure looked like the one thing that had gone right. Probing
now runs a real query as `ghost` (and a real PING for Redis), falls back to
a free port, and repairs a stale `.env` instead of leaving it wrong.
3. A run whose worker died stayed RUNNING forever. The lease correctly lets a
new job take over an expired one, but nothing produced that job: when a
worker crashed or was redeployed mid-run, the BullMQ job died with it and
the row sat RUNNING with an expired lease indefinitely — a run in progress
that no process was working on. `reclaimRuns.ts` sweeps on boot and every
minute. Restarting is safe because run position is folded from the
append-only journal, so a completed step is not re-executed; past five
attempts the run becomes an INCIDENT for a human rather than looping.
Adds `pnpm check`: read-only, and it names these two ("NO WORKER RUNNING",
"something IS listening there — most likely a different Postgres") rather than
leaving them to be inferred.
Verified on this machine, not just in CI: `pnpm check` clean, the e2e driver
halts at the gate, captures session state, resumes, clicks submit exactly once
and reaches SUCCEEDED; two runs stuck RUNNING for 20 hours were reclaimed on
boot and correctly refused an unsafe resume (OUTCOME_UNKNOWN / RESTORE_UNSAFE)
rather than re-clicking. typecheck 5/5, 430 tests, build 4/4.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…al env docs Signed-out visitors previously landed straight on /signin with no explanation of what Ghost does; now they see the actual pipeline, positioning, and target verticals before being asked to sign up. Also routes Auth.js's sign-out confirmation through a themed page instead of its unstyled built-in one, and documents the REDIS_URL/GHOST_SESSION_KEY env vars pnpm test silently needs (their absence surfaces as 429s/INCIDENT status, not a missing-var error). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
cloud/packages/core/src/recording/ingest.ts was staged in the index by in-progress work from a parallel session (not part of the landing-page/ sign-out/docs change). It got committed as a byproduct of `git add` committing the whole index rather than just the intended files. Untrack it again so it goes back to being that other work's own untracked file, with no change to its on-disk content. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Production had no S3_BUCKET/GHOST_ARTIFACT_DIR configured, so artifactStore() fell back to writing to local disk — which doesn't work on Vercel's read-only serverless filesystem and made every recording upload 500. Add VercelBlobArtifactStore (private access, reads always go through the app's own artifact route since signed URLs aren't safe to hand out for a private store) and prefer it when BLOB_READ_WRITE_TOKEN is set. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
globals.css and layout.tsx got swept into the prior commit by a race with another concurrent agent process editing this same working tree (staged those two files between my `git add` and `git commit`). Neither belongs to the Blob-storage change that commit describes, and I never reviewed their content. Restoring both to their pre-commit state here; their in-progress edits are reapplied to the working tree, uncommitted, in the next step. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
get() can return {stream: null} on a 304 (or null entirely if not
found); narrow on result?.stream so a missing/empty read fails loudly
instead of iterating a null stream. Caught by tsc, not runtime.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
master shipped a substantially more complete landing page and dashboard (#414, #415) while this branch had its own smaller landing-page pass in flight — same files (page.tsx, dashboard/page.tsx, globals.css). Resolved in favor of master's version everywhere they overlapped: it's more honest (built/not-built status, the "is this just browser automation" section), fixes a real dark-mode CSS bug (@theme nested inside @media doesn't survive Tailwind v4's hoisting), and renders real org data on the dashboard instead of static nav cards. Dropped the Fraunces/IBM Plex Mono font experiment from layout.tsx/globals.css along with it, since nothing references those tokens anymore and master's design intentionally stays on one font. Kept from this branch: the themed sign-out page (signout/page.tsx, auth.ts pages.signOut) and the CLAUDE.md env-var doc fix, neither of which master touched.
The DOM ReadableStream type in this tsconfig's lib (ES2022, no DOM) has no Symbol.asyncIterator, so `for await` over it doesn't typecheck even though it works at runtime on Node 24. Route through Readable.fromWeb instead, which is typed and portable regardless of tsconfig lib settings. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
The regex required `providers.push(GitHub)` — a bare reference — but this
branch legitimately calls `GitHub({ allowDangerousEmailAccountLinking: true })`
(needed since the Resend adapter went in: without it, Auth.js refuses a
sign-in whose email already belongs to a different provider's account).
Match on the provider name after `providers.push(`, not on whether it's
invoked — the actual guarantee under test (each provider gated behind its
own env var) is unchanged.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…n auth-providers.test.ts)
3 tasks
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
RESEND_API_KEY/RESEND_EMAIL_DOMAIN), following the existing env-var-gated pattern. Wires the Prisma adapter (only when Resend is configured) andallowDangerousEmailAccountLinkingon GitHub/Google to preserve the existing merge-by-email behavior.Test plan
pnpm typecheck(all packages) — cleanpnpm --filter @ghost/web exec eslinton changed files — cleanpnpm test(Postgres + Redis running) — 5/5 packages greenpnpm --filter @ghost/web build— succeeds both with and without Resend env vars set/renders real content locally (next start, curl)🤖 Generated with Claude Code