1.6.29+ changes SSO org assignment: Google/GitHub sign-ins no longer
auto-join the org by email domain without domainVerification, and
SSO sign-ins skip membership when an invitation is pending.
Use execFile arguments for local login and the SSH stream for remote login. Discard execution errors after sanitizing their messages so echoed credentials cannot survive in nested error fields.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
GitLab invalidates a refresh token when it is used. Two deployments that found the token expired at the same time sent the same refresh token, and the second one failed with "Failed to refresh token: Bad Request".
Trials were always Hobby, so a trial user who wanted Startup had to go
through Checkout, which created a second subscription on the same
customer. Seven days later Stripe cancelled the trial sub and the webhook,
matching only on customer id, zeroed serversQuantity and deactivated the
servers of someone who was already paying.
The trial now starts on the plan the user chooses and switching plans
mutates that same subscription, so a customer never holds two.
- startFreeTrial takes a tier; TRIAL_SERVER_LIMIT becomes TRIAL_SERVER_LIMITS
(Hobby 1 server, Startup 3)
- onboarding offers both plans with a trial and a subscribe button
- billing exposes Switch to Startup/Hobby during the trial and hides the
checkout CTAs and server steppers that would create a second subscription
- upgradeSubscription caps the server count to the plan's included servers
while trialing and skips proration, which emitted an orphan invoice
- webhook treats trialing as active instead of disabling servers, and
guards invoices that carry no subscription
- getBillingStatus reports hasPaymentMethod, so the banners stop asking for
a card once one is on file
- new cloud-only daily job emails trial users without a payment method at 3
and 1 days left, deduped through subscription metadata claimed before the
send so concurrent replicas cannot double-send
Self-service deletion from Settings > Profile confirmed with the account
password (when present) plus a 6-digit code sent by email, and a root-only
"delete by email" tool in the impersonation bar to fulfil GDPR requests.
Both paths run the same flow: cancel every Stripe subscription (customer is
kept for invoicing), delete the user and all owned organizations in one
transaction, anonymize audit trail left in other organizations, reassign
pending invitations to the org owner, remove the HubSpot contact when
HUBSPOT_ACCESS_TOKEN is configured, and log a JSON deletion record.
Also blocks better-auth's raw /admin/remove-user endpoint and makes the
Stripe webhook tolerate subscription/customer events for already deleted
users.