Félix Malfait b8a3399230 fix(billing): link billing emails to the workspace subdomain (#22401)
## Problem

Billing and workspace-suspension emails hardcoded a
`BILLING_SETTINGS_URL` constant pointing at
`https://app.twenty.com/settings/billing`. A user in
`myworkspace.twenty.com` therefore received a CTA that bounced through
the central `app` domain instead of landing on their own workspace.
Those cross-subdomain redirects are unreliable, so it's better to link
straight to the workspace.

The invite, password-reset and email-verification emails already do this
correctly by building a workspace-specific URL server-side with
`WorkspaceDomainsService.buildWorkspaceURL(...)`; the billing/suspension
senders had the `workspace` entity in scope but never used it.

## Fix

Build the billing settings URL server-side and pass it into the
templates as a `link` prop, mirroring the existing pattern:

- **Templates** now take a `link` prop instead of the hardcoded
constant: `billing-trial-ending`, `billing-trial-converting`,
`billing-subscription-renewing`, `warn-suspended-workspace`.
- **`BillingReminderService`** and **`CleanerWorkspaceService`** build
`buildWorkspaceURL({ workspace, pathname:
getSettingsPath(SettingsPath.Billing) })` and thread it through.
- Wired `WorkspaceDomainsModule` into both NestJS modules; deleted the
now-unused `billing-settings-url.constant.ts`; updated the reminder unit
test.

This also fixes **self-hosted** deployments, which previously got the
same wrong hardcoded `app.twenty.com` link.

### Intentionally unchanged

- `clean-suspended-workspace` keeps its central-domain "start a new
workspace" CTA — that workspace is already deleted, so its subdomain no
longer resolves.
- `password-update-notify` (not a billing email) still uses
`getBaseUrl()`; the workspace entity isn't readily loaded there. Can be
a follow-up.

## Testing

Extended `billing-reminder.service.spec.ts` to assert the
workspace-specific `link` is threaded into the email. Note: local
`typecheck`/tests could not be run because the sandbox proxy repeatedly
dropped `yarn install` mid-fetch; the diff was reviewed line-by-line and
import paths verified against the actual `twenty-shared` exports and
module wiring. CI will provide the authoritative check.

https://claude.ai/code/session_01QsgNd4SWdcRkPFyrnCgj2b

---
_Generated by [Claude
Code](https://claude.ai/code/session_01QsgNd4SWdcRkPFyrnCgj2b)_

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/22401?utm_source=github"
target="_blank" rel="noopener noreferrer"
data-no-image-dialog="true"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source
media="(prefers-color-scheme: light)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img
alt="Review in cubic"
src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a>
<!-- End of auto-generated description by cubic. -->
2026-07-01 14:28:36 +00:00
2026-06-11 11:02:28 +02:00

Twenty logo

The #1 Open-Source CRM

Website · Documentation · Roadmap · Discord · Figma

Twenty banner


Why Twenty

Twenty gives technical teams the building blocks for a custom CRM that meets complex business needs and quickly adapts as the business evolves. Twenty is the CRM you build, ship, and version like the rest of your stack.

Learn more about why we built Twenty


Installation

Cloud

The fastest way to get started. Sign up at twenty.com and spin up a workspace in under a minute, with no infrastructure to manage and always up to date.

Build an app

Scaffold a new app with the Twenty CLI:

npx create-twenty-app my-app

Define objects, fields, and views as code:

import { defineObject, FieldType } from 'twenty-sdk/define';

export default defineObject({
  nameSingular: 'deal',
  namePlural: 'deals',
  labelSingular: 'Deal',
  labelPlural: 'Deals',
  fields: [
    { name: 'name', label: 'Name', type: FieldType.TEXT },
    { name: 'amount', label: 'Amount', type: FieldType.CURRENCY },
    { name: 'closeDate', label: 'Close Date', type: FieldType.DATE_TIME },
  ],
});

Then ship it to your workspace:

npx twenty app:publish --private

See the app development guide for objects, views, agents, and logic functions.

Self-hosting

Run Twenty on your own infrastructure with Docker Compose, or contribute locally via the local setup guide.



Everything you need

Twenty gives you the building blocks of a modern CRM (objects, views, workflows, and agents) and lets you extend them as code. Here's a tour of what's in the box.

Want to go deeper? Read the User Guide for product walkthroughs, or the Documentation for developer reference.

Create your apps

Learn more about apps in doc

Stay on top with version control

Learn more about version control in doc

All the tools you need to build anything

Learn more about primitives in doc

Customize your layouts

Learn more about layouts in doc

AI agents and chats

Learn more about AI in doc

Plus all the tools of a good CRM

Learn more about CRM features in doc


Stack

Thanks

Greptile      Sentry      Crowdin

Thanks to these amazing services that we use and recommend for code review (Greptile), catching bugs (Sentry) and translating (Crowdin).

Join the Community

Star the repo · Discord · Feature requests · Releases · X · LinkedIn · Crowdin · Contribute

S
Description
The open alternative to Salesforce, designed for AI.
Readme AGPL-3.0 1.4 GiB
Languages
TypeScript 79.6%
MDX 17.3%
JavaScript 2.7%
Python 0.2%
SCSS 0.1%