Skip to content

Commit 8e6328c

Browse files
committed
Add domain transformation support for split-domain architectures
1 parent 9a9ff76 commit 8e6328c

15 files changed

Lines changed: 739 additions & 71 deletions

File tree

.changeset/major-sites-roll.md

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
---
2+
'@shopify/shopify-api': major
3+
'@shopify/shopify-app-express': minor
4+
---
5+
6+
**BREAKING CHANGE**: Removed `customShopDomains` configuration parameter. Use `domainTransformations` instead, which provides both validation and transformation capabilities.
7+
8+
The `SHOP_CUSTOM_DOMAIN` environment variable is no longer supported.
9+
10+
**Migration Guide**:
11+
12+
If you were using `customShopDomains` for validation only:
13+
14+
```typescript
15+
// Before
16+
shopifyApi({
17+
customShopDomains: ['custom\.domain\.com']
18+
})
19+
20+
// After
21+
shopifyApi({
22+
domainTransformations: [{
23+
match: /^([a-zA-Z0-9][a-zA-Z0-9-_]*)\.custom\.domain\.com$/,
24+
transform: '$1.custom.domain.com'
25+
}]
26+
})
27+
```

packages/apps/shopify-api/docs/reference/shopifyApi.md

Lines changed: 62 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,56 @@ Fixed Storefront API access token for private apps.
108108

109109
Use this if you need to allow values other than `myshopify.com`.
110110

111+
### domainTransformations
112+
113+
`DomainTransformation[]` | Defaults to `undefined`
114+
115+
Configure domain transformations for split-domain architectures (e.g., separate admin and API domains). Useful for local development environments.
116+
117+
Each transformation can use either template strings with capture groups (`$1`, `$2`, etc.) or custom functions for complex logic.
118+
119+
**Template string example:**
120+
121+
```ts
122+
domainTransformations: [
123+
{
124+
match: /^([a-zA-Z0-9][a-zA-Z0-9-_]*)\.my\.shop\.dev$/,
125+
transform: '$1.dev-api.shop.dev',
126+
},
127+
];
128+
```
129+
130+
**Function-based example:**
131+
132+
```ts
133+
domainTransformations: [
134+
{
135+
match: /^([a-zA-Z0-9-_]+)\.admin\.example\.com$/,
136+
transform: (matches) => {
137+
const shopName = matches[1];
138+
return shopName.startsWith('test-')
139+
? `${shopName}.api-staging.example.com`
140+
: `${shopName}.api.example.com`;
141+
},
142+
},
143+
];
144+
```
145+
146+
**Interface:**
147+
148+
```ts
149+
interface DomainTransformation {
150+
// Pattern to match source domain
151+
match: RegExp | string;
152+
153+
// Transformation: function or template string with $1, $2 capture groups
154+
transform: ((matches: RegExpMatchArray) => string | null) | string;
155+
156+
// Whether to include transformed domains in host validation (default: true)
157+
includeHost?: boolean;
158+
}
159+
```
160+
111161
### billing
112162

113163
`BillingConfig` | Defaults to `undefined`
@@ -164,17 +214,17 @@ Whether to add the current timestamp to every logged message.
164214

165215
This function returns an object containing the following properties:
166216

167-
| Property | Description |
168-
| ----------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------- |
169-
| config | The options used to set up the object, containing the parameters of this function. |
170-
| [auth](./auth/README.md) | Object containing functions to authenticate with Shopify APIs. |
171-
| [clients](./clients/README.md) | Object containing clients to access Shopify APIs. |
172-
| [session](./session/README.md) | Object containing functions to manage Shopify sessions. |
173-
| [webhooks](./webhooks/README.md) | Object containing functions to configure and handle Shopify webhooks. |
174-
| [billing](./billing/README.md) | Object containing functions to enable apps to bill merchants. |
175-
| [flow](./flow/README.md) | Object containing functions to authenticate Flow extension requests.
176-
| [fulfillment service](./fulfillment-service/README.md) | Object containing functions to authenticate and create fulfillment service requests. |
177-
| [utils](./utils/README.md) | Object containing general functions to help build apps. |
178-
| [rest](../guides/rest-resources.md) | Object containing OO representations of the Admin REST API. See the [API reference documentation](https://shopify.dev/docs/api/admin-rest) for details. |
217+
| Property | Description |
218+
| ------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------- |
219+
| config | The options used to set up the object, containing the parameters of this function. |
220+
| [auth](./auth/README.md) | Object containing functions to authenticate with Shopify APIs. |
221+
| [clients](./clients/README.md) | Object containing clients to access Shopify APIs. |
222+
| [session](./session/README.md) | Object containing functions to manage Shopify sessions. |
223+
| [webhooks](./webhooks/README.md) | Object containing functions to configure and handle Shopify webhooks. |
224+
| [billing](./billing/README.md) | Object containing functions to enable apps to bill merchants. |
225+
| [flow](./flow/README.md) | Object containing functions to authenticate Flow extension requests. |
226+
| [fulfillment service](./fulfillment-service/README.md) | Object containing functions to authenticate and create fulfillment service requests. |
227+
| [utils](./utils/README.md) | Object containing general functions to help build apps. |
228+
| [rest](../guides/rest-resources.md) | Object containing OO representations of the Admin REST API. See the [API reference documentation](https://shopify.dev/docs/api/admin-rest) for details. |
179229

180230
[Back to reference index](./README.md)

packages/apps/shopify-api/docs/reference/utils/sanitizeShop.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
This method makes user inputs safer by ensuring that a given shop value is a properly formatted Shopify shop domain.
44

5-
> **Note**: if you're using custom shop domains for testing, you can use the `customShopDomains` setting to add allowed domains.
5+
> **Note**: if you're using custom shop domains for testing, you can use the `customShopDomains` setting to add allowed domains. For split-domain architectures (e.g., local development with separate admin and API domains), use `domainTransformations` to configure domain mappings that will be automatically applied during sanitization.
66
77
## Example
88

packages/apps/shopify-api/lib/__tests__/test-config.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,6 @@ const TEST_CONFIG = {
6868
apiVersion: ApiVersion.July25,
6969
isEmbeddedApp: false,
7070
isCustomStoreApp: false,
71-
customShopDomains: undefined,
7271
billing: undefined,
7372
restResources: undefined,
7473
logger: {

packages/apps/shopify-api/lib/auth/get-embedded-app-url.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ export function buildEmbeddedAppUrl(
4545
config: ConfigInterface,
4646
): BuildEmbeddedAppUrl {
4747
return (host: string): string => {
48-
sanitizeHost()(host, true);
48+
sanitizeHost(config)(host, true);
4949
const decodedHost = decodeHost(host);
5050

5151
return `https://${decodedHost}/apps/${config.apiKey}`;

packages/apps/shopify-api/lib/base-types.ts

Lines changed: 26 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import {ShopifyRestResources} from '../rest/types';
33

44
import {AuthScopes} from './auth/scopes';
55
import {BillingConfig} from './billing/types';
6-
import {ApiVersion, LogSeverity} from './types';
6+
import {ApiVersion, DomainTransformation, LogSeverity} from './types';
77

88
/**
99
* A function used by the library to log events related to Shopify.
@@ -69,9 +69,31 @@ export interface ConfigParams<
6969
*/
7070
privateAppStorefrontAccessToken?: string;
7171
/**
72-
* Override values for Shopify shop domains.
73-
*/
74-
customShopDomains?: (RegExp | string)[];
72+
* Custom domain transformations for split-domain architectures.
73+
*
74+
* Transformations are applied in order. The first matching transformation is used.
75+
* If no transformation matches, the domain is validated as-is.
76+
*
77+
* @example
78+
* // Simple template-based transformation
79+
* domainTransformations: [{
80+
* match: /^([a-zA-Z0-9][a-zA-Z0-9-_]*)\.my\.shop\.dev$/,
81+
* transform: '$1.dev-api.shop.dev'
82+
* }]
83+
*
84+
* @example
85+
* // Function-based transformation with custom logic
86+
* domainTransformations: [{
87+
* match: /^([^.]+)\.ui\.example\.com$/,
88+
* transform: (matches) => {
89+
* const shopName = matches[1];
90+
* return shopName.startsWith('test-')
91+
* ? `${shopName}.api-staging.example.com`
92+
* : `${shopName}.api.example.com`;
93+
* }
94+
* }]
95+
*/
96+
domainTransformations?: DomainTransformation[];
7597
/**
7698
* Billing configurations for the app.
7799
*/

packages/apps/shopify-api/lib/config.ts

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,6 @@ export function validateConfig<Params extends ConfigParams>(
6464
userAgentPrefix,
6565
logger,
6666
privateAppStorefrontAccessToken,
67-
customShopDomains,
6867
billing,
6968
future,
7069
...mandatoryParams
@@ -89,7 +88,6 @@ export function validateConfig<Params extends ConfigParams>(
8988
logger: {...config.logger, ...(logger || {})},
9089
privateAppStorefrontAccessToken:
9190
privateAppStorefrontAccessToken ?? config.privateAppStorefrontAccessToken,
92-
customShopDomains: customShopDomains ?? config.customShopDomains,
9391
billing: billing ?? config.billing,
9492
future: future ?? config.future,
9593
});

packages/apps/shopify-api/lib/types.ts

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -125,3 +125,41 @@ export enum Method {
125125
Options = 'OPTIONS',
126126
Connect = 'CONNECT',
127127
}
128+
129+
/**
130+
* Configuration for transforming shop domains in split-domain architectures.
131+
*
132+
* @example
133+
* // Template-based transformation
134+
* {
135+
* match: /^([a-zA-Z0-9][a-zA-Z0-9-_]*)\.my\.shop\.dev$/,
136+
* transform: '$1.dev-api.shop.dev'
137+
* }
138+
*
139+
* @example
140+
* // Function-based transformation
141+
* {
142+
* match: /^([^.]+)\.ui\.example\.com$/,
143+
* transform: (matches) => `${matches[1]}.api.example.com`
144+
* }
145+
*/
146+
export interface DomainTransformation {
147+
/**
148+
* Pattern to match against shop domains (source domain).
149+
* Can be a RegExp or string (converted to RegExp internally).
150+
*/
151+
match: RegExp | string;
152+
153+
/**
154+
* Transformation function or template string.
155+
* - Template string: Uses $1, $2, etc. for capture group substitution
156+
* - Function: Receives regex match groups and returns transformed domain
157+
*/
158+
transform: ((matches: RegExpMatchArray) => string | null) | string;
159+
160+
/**
161+
* Whether this transformation should also apply to host validation.
162+
* @default true
163+
*/
164+
includeHost?: boolean;
165+
}

0 commit comments

Comments
 (0)