Update the encryption sections in the agent notes, internal API auth
doc, OAuth setup doc, and architecture notes, and state explicitly that
per-user DEKs must not be reintroduced.
OAuth access/refresh tokens and pasted credential configs are sealed
and opened with the connection's OrganizationID instead of
ConnectedByUserID, which is now attribution-only. An org's CRM and
Slack connections keep working when the user who connected them is
removed from the organization.
SMTP/IMAP passwords on the validation round-trip are now encrypted and
decrypted under the organization DEK, carried as OrgID on
EventWorkerEmailValidation. Onboarding requires an organization before
a mailbox can be validated.
This fixes a latent key mismatch: the payload never set UserID, so the
worker decrypted with the zero-UUID platform key while the backend had
encrypted with the user key.
The send pipeline (publisher subject/body encryption, the S3 emsg blob,
and the worker-side decrypt) now keys off models.SendEmail.OrgID
instead of UserID. EmailMessage loses its UserID field entirely:
emailSender.Send derives the cipher identity from the email account's
OrganizationID and refuses to send for an account without one.
This also fixes two latent bugs. Emails sent through user_email_task
never set UserID, so they were silently encrypted under the zero-UUID
platform key. campaign_task's discarded Encrypt() pair is replaced with
an explicit DEK warm that fails fast when KMS is unavailable.
cipher.CipherService.Cipher(ctx, orgID) now resolves, generates, and
caches DEKs per organization (Redis key decrypted_key:<orgID>).
Platform-level secrets keep the zero-UUID identity, renamed to
platformCipherID since it no longer partitions against user keys.
The hand-rolled flagship page now reads the shared learn registry for its
rail (it had drifted and was missing an essay), emits the BlogPosting
JSON-LD every other essay already had, and corrects its repeated 100/day
per-mailbox claim: the product default is 50/day, with 100 only as the
validation ceiling under positive reputation evidence.
The /api/v1/internal/dek route parameter is now :orgID to match the
org-scoped encryptedkeys store. Workers fetch the organization DEK for
the account they are operating on instead of a user DEK.
Every essay passes a real display headline instead of surfacing its SEO
title as the H1. Repairs roughly forty sentences left broken by an
earlier em-dash strip ('The safest assumption. for them. is spammer.')
using colons, commas and real sentence splits. Drops the internal DEK
term from the customer-facing glossary.
Mailboxes, integration tokens, and message content are organization
assets. Keying their DEK by the connecting user meant offboarding that
user made every ciphertext they created unreadable.
Migration 000039 drops user_encrypted_keys and creates
organization_encrypted_keys keyed by organization_id. The new table has
no FK on purpose: platform secrets live under the zero UUID (no
organizations row), and DEK rows must never cascade-delete because a
lost DEK is unrecoverable.
Pre-production, so there is no data migration; ciphertexts sealed under
the old per-user DEKs are abandoned with the table.
Adds a headline prop so the visible H1 stops rendering the SEO title
suffix ('... | Warmbly'), real prev/next navigation from the registry, a
sticky top bar with reading progress, a mobile on-this-page disclosure
(there was no TOC below desktop), hover heading anchors, and the shared
.article-prose typography. The rail no longer flashes 'Loading...'.
One registry in src/lib/learn.ts now drives the index, the article rails
and prev/next ordering; previously three hand-maintained copies had
drifted (missing personalization, wrong read times). Fixes stale facts on
the index: 'Six essays' with seven listed, a fabricated 42-term glossary
count, and renders the previously dead level chips and summaries on rows.
Mailbox send budgets vs server limits, the early-quarantine bands that
protect shared warmup pools, and a plain reading of Google's bulk sender
rules. All grounded in the product's real defaults (50/day cap, 600s
spacing, the 10/20/40/80 placement bands).
Blog joins the header nav (desktop and mobile pick it up from the shared
nav array) and the footer Company column, with a small RSS link in the
footer bottom bar.
Description-only feed at /blog/rss.xml (full-content RSS cannot render
MDX components), excluding drafts unconditionally, plus an autodiscovery
link in the global head. The feed is the subscription: no newsletter.
Compact header, then a cover-image card grid with topic tabs, client-side
search and batched load-more. Every card is server-rendered; search and
load-more only toggle visibility, so crawlers and no-JS readers see the
full archive. Posts without a cover image get deterministic generated sky
art seeded from the slug. Tags get their own filtered pages.
Article shell with a sticky meta bar carrying reading progress, a right
rail (chapter scroll-spy built from server-rendered heading ids, post
facts, neighbors), prev/next cards and BlogPosting JSON-LD with real
publish dates. MDX posts can embed the Callout component.
Extracts the Learn layout's arbitrary-variant prose blob into a single
.article-prose class, registered in @layer components so page-level
utilities can still override it. Fixes the invalid counter-reset syntax
that rendered every ordered-list item as 00., adds table styles, hover
heading anchors and a reading-progress bar.
Posts are markdown/MDX files under src/content/blog with a zod-validated
schema (title, description, pubDate, tags, optional cover, draft). All
listing surfaces read through getPublishedPosts() so draft filtering has
a single owner. @astrojs/mdx is pinned to ^4.x: the npm latest (v6)
requires Astro 6 and would break this tree.
The node editor (trigger/condition/action) and the test-run/history panel
used to pop in and out with no transition. They now slide in from the
right with the same spring the app's other sheets use (AnimatePresence,
stiffness 300 / damping 32), the editor body fades when switching nodes,
the action config fields cross-fade when picking a different action or
target, and the insights panel body fades when toggling test vs history.
Replace the hand-maintained changelog entries with a build-time fetch of the
real merged pull requests on warmbly/warmbly. PRs (not commits) are the unit,
so each entry is one shippable change rather than one of many commits.
src/lib/changelog.ts fetches closed PRs into main from the GitHub REST API,
keeps the merged ones, strips conventional-commit prefixes, derives the tag
from the type, and pulls the body from the PR's Summary bullets. Marketing-site
and pure-infra PRs are filtered by type, title pattern, and a skip-changelog
label, all configurable in one place. Results are cached per build and fall
back to a curated list if the API is unreachable, so the build never breaks.
The changelog page and the homepage ChangelogShowcase both consume this data
and link each entry to its pull request. Set GITHUB_TOKEN at build to lift the
unauthenticated rate limit; it is optional for the public repo.
Delete the public roadmap page and every reference to it (footer, open-source
showcase, about, changelog, trust). The roadmap was built from seeded,
fabricated items.
Rebuild the changelog from the real git history. The previous entries were
self-described seeded fabrications and contradicted the codebase (claiming
SAML SSO and BYOK shipped, which they have not). There are no releases, tags,
or CHANGELOG.md, so the commit log is the source of truth. Group the work into
a handful of milestone entries rather than one per release, mirror them on the
homepage changelog showcase, and drop the invented version numbers.
Fix the broken links the old page carried: the dead /changelog/feed.xml RSS
feed and the non-existent CHANGELOG.md now point at the real commit history.
Relabel the trust page's 'roadmap' posture state to 'planned' so SOC 2 and
ISO 27001 stay honestly marked as not yet in place.
Add the run-another-automation native action to the builder, richer action visuals, task assignment fields, disabled-target warnings, and unsaved-change tracking for the automation editor.
Use the shared assignee/team picker in campaign action steps, persist task_assigned_team_id, and make run-automation campaign steps surface disabled and trigger-mismatch warnings.
Support chained automation execution from native actions, guard automation recursion depth, carry idempotency context through campaign-launched automations, allow team task assignment in backend action execution, and reject deletes while campaign steps still reference an automation.
Refresh the marketing CRM page with a fuller product walkthrough, live pipeline mock, attribution details, task views, contact suppression, data model coverage, CRM sync, and updated FAQ/CTA copy.
Restructure the docs navigation into product-focused pages, add automation expression reference content, and refresh related template and personalization surfaces.
Refresh the analytics marketing page around actionable deliverability metrics, realtime dashboard behavior, export surfaces, and a share-card mock that mirrors the product UI.
Refresh the campaigns landing page around the visual sequence builder, branching logic, A/B variants, sender pool distribution, and updated safety defaults. Add the icon paths needed by the new page sections.