Skip to content

Commit d28ea25

Browse files
authored
feat(api, dashboard, docs, sdk): default region for organization (#2792)
Signed-off-by: fabjanvucina <[email protected]>
1 parent ee1ba4a commit d28ea25

File tree

23 files changed

+131
-42
lines changed

23 files changed

+131
-42
lines changed

apps/api/src/app.service.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -153,7 +153,9 @@ Admin user created with API key: ${value}
153153
}
154154

155155
private async initializeBackupRegistry(): Promise<void> {
156-
const existingRegistry = await this.dockerRegistryService.getAvailableBackupRegistry('us')
156+
const existingRegistry = await this.dockerRegistryService.getAvailableBackupRegistry(
157+
this.configService.getOrThrow('defaultRegion'),
158+
)
157159
if (existingRegistry) {
158160
return
159161
}

apps/api/src/config/configuration.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -202,6 +202,7 @@ const configuration = {
202202
maxSnapshotSize: parseInt(process.env.DEFAULT_ORG_QUOTA_MAX_SNAPSHOT_SIZE || '20', 10),
203203
volumeQuota: parseInt(process.env.DEFAULT_ORG_QUOTA_VOLUME_QUOTA || '100', 10),
204204
},
205+
defaultRegion: process.env.DEFAULT_REGION || 'us',
205206
}
206207

207208
export { configuration }
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
/*
2+
* Copyright 2025 Daytona Platforms Inc.
3+
* SPDX-License-Identifier: AGPL-3.0
4+
*/
5+
6+
import { MigrationInterface, QueryRunner } from 'typeorm'
7+
import { configuration } from '../config/configuration'
8+
9+
export class Migration1761912147638 implements MigrationInterface {
10+
name = 'Migration1761912147638'
11+
12+
public async up(queryRunner: QueryRunner): Promise<void> {
13+
await queryRunner.query(`ALTER TABLE "organization" ADD "defaultRegion" character varying NULL`)
14+
await queryRunner.query(`UPDATE "organization" SET "defaultRegion" = '${configuration.defaultRegion}'`)
15+
await queryRunner.query(`ALTER TABLE "organization" ALTER COLUMN "defaultRegion" SET NOT NULL`)
16+
17+
await queryRunner.query(`ALTER TABLE "sandbox" ALTER COLUMN "region" DROP DEFAULT`)
18+
await queryRunner.query(`ALTER TABLE "warm_pool" ALTER COLUMN "target" DROP DEFAULT`)
19+
}
20+
21+
public async down(queryRunner: QueryRunner): Promise<void> {
22+
await queryRunner.query(`ALTER TABLE "organization" DROP COLUMN "defaultRegion"`)
23+
24+
await queryRunner.query(`ALTER TABLE "sandbox" ALTER COLUMN "region" SET DEFAULT 'us'`)
25+
await queryRunner.query(`ALTER TABLE "warm_pool" ALTER COLUMN "target" SET DEFAULT 'us'`)
26+
}
27+
}

apps/api/src/organization/dto/organization.dto.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,11 @@ export class OrganizationDto {
9898
})
9999
sandboxLimitedNetworkEgress: boolean
100100

101+
@ApiProperty({
102+
description: 'Default region',
103+
})
104+
defaultRegion: string
105+
101106
static fromOrganization(organization: Organization): OrganizationDto {
102107
const dto: OrganizationDto = {
103108
id: organization.id,
@@ -118,6 +123,7 @@ export class OrganizationDto {
118123
maxMemoryPerSandbox: organization.maxMemoryPerSandbox,
119124
maxDiskPerSandbox: organization.maxDiskPerSandbox,
120125
sandboxLimitedNetworkEgress: organization.sandboxLimitedNetworkEgress,
126+
defaultRegion: organization.defaultRegion,
121127
}
122128

123129
return dto

apps/api/src/organization/entities/organization.entity.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,9 @@ export class Organization {
2929
})
3030
telemetryEnabled: boolean
3131

32+
@Column()
33+
defaultRegion: string
34+
3235
@Column({
3336
type: 'int',
3437
default: 10,
@@ -152,4 +155,8 @@ export class Organization {
152155
type: 'timestamp with time zone',
153156
})
154157
updatedAt: Date
158+
159+
constructor(defaultRegion: string) {
160+
this.defaultRegion = defaultRegion
161+
}
155162
}

apps/api/src/organization/services/organization.service.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -228,7 +228,9 @@ export class OrganizationService implements OnModuleInit, TrackableJobExecutions
228228
throw new ForbiddenException('You have reached the maximum number of created organizations')
229229
}
230230

231-
let organization = new Organization()
231+
const defaultRegion = this.configService.getOrThrow('defaultRegion')
232+
233+
let organization = new Organization(defaultRegion)
232234

233235
organization.name = createOrganizationDto.name
234236
organization.createdBy = createdBy

apps/api/src/sandbox/entities/sandbox.entity.ts

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -36,9 +36,7 @@ export class Sandbox {
3636
@Column()
3737
name: string
3838

39-
@Column({
40-
default: 'us',
41-
})
39+
@Column()
4240
region: string
4341

4442
@Column({
@@ -196,10 +194,11 @@ export class Sandbox {
196194
@Column({ nullable: true })
197195
daemonVersion?: string
198196

199-
constructor(name?: string) {
197+
constructor(region: string, name?: string) {
200198
this.id = uuidv4()
201199
// Set name - use provided name or fallback to ID
202200
this.name = name || this.id
201+
this.region = region
203202
}
204203

205204
public setBackupState(

apps/api/src/sandbox/entities/warm-pool.entity.ts

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,9 +17,7 @@ export class WarmPool {
1717
@Column()
1818
snapshot: string
1919

20-
@Column({
21-
default: 'us',
22-
})
20+
@Column()
2321
target: string
2422

2523
@Column()

apps/api/src/sandbox/services/sandbox.service.ts

Lines changed: 8 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -232,11 +232,10 @@ export class SandboxService {
232232
}
233233

234234
async createForWarmPool(warmPoolItem: WarmPool): Promise<Sandbox> {
235-
const sandbox = new Sandbox()
235+
const sandbox = new Sandbox(warmPoolItem.target)
236236

237237
sandbox.organizationId = SANDBOX_WARM_POOL_UNASSIGNED_ORGANIZATION
238238

239-
sandbox.region = warmPoolItem.target
240239
sandbox.class = warmPoolItem.class
241240
sandbox.snapshot = warmPoolItem.snapshot
242241
// TODO: default user should be configurable
@@ -281,7 +280,7 @@ export class SandboxService {
281280
let pendingDiskIncrement: number | undefined
282281

283282
try {
284-
const region = this.getValidatedOrDefaultRegion(createSandboxDto.target)
283+
const region = this.getValidatedOrDefaultRegion(organization, createSandboxDto.target)
285284
const sandboxClass = this.getValidatedOrDefaultClass(createSandboxDto.class)
286285

287286
let snapshotIdOrName = createSandboxDto.snapshot
@@ -362,7 +361,7 @@ export class SandboxService {
362361
const warmPoolSandbox = await this.warmPoolService.fetchWarmPoolSandbox({
363362
organizationId: organization.id,
364363
snapshot: snapshotIdOrName,
365-
target: createSandboxDto.target,
364+
target: region,
366365
class: createSandboxDto.class,
367366
cpu: cpu,
368367
mem: mem,
@@ -386,12 +385,11 @@ export class SandboxService {
386385
snapshotRef: snapshot.internalName,
387386
})
388387

389-
const sandbox = new Sandbox(createSandboxDto.name)
388+
const sandbox = new Sandbox(region, createSandboxDto.name)
390389

391390
sandbox.organizationId = organization.id
392391

393392
// TODO: make configurable
394-
sandbox.region = region
395393
sandbox.class = sandboxClass
396394
sandbox.snapshot = snapshot.name
397395
// TODO: default user should be configurable
@@ -523,7 +521,7 @@ export class SandboxService {
523521
let pendingDiskIncrement: number | undefined
524522

525523
try {
526-
const region = this.getValidatedOrDefaultRegion(createSandboxDto.target)
524+
const region = this.getValidatedOrDefaultRegion(organization, createSandboxDto.target)
527525
const sandboxClass = this.getValidatedOrDefaultClass(createSandboxDto.class)
528526

529527
const cpu = createSandboxDto.cpu || DEFAULT_CPU
@@ -551,11 +549,10 @@ export class SandboxService {
551549
await this.volumeService.validateVolumes(organization.id, volumeIdOrNames)
552550
}
553551

554-
const sandbox = new Sandbox(createSandboxDto.name)
552+
const sandbox = new Sandbox(region, createSandboxDto.name)
555553

556554
sandbox.organizationId = organization.id
557555

558-
sandbox.region = region
559556
sandbox.class = sandboxClass
560557
sandbox.osUser = createSandboxDto.user || 'daytona'
561558
sandbox.env = createSandboxDto.env || {}
@@ -1082,9 +1079,9 @@ export class SandboxService {
10821079
}
10831080
}
10841081

1085-
private getValidatedOrDefaultRegion(region?: string): string {
1082+
private getValidatedOrDefaultRegion(organization: Organization, region?: string): string {
10861083
if (!region || region.trim().length === 0) {
1087-
return 'us'
1084+
return organization.defaultRegion
10881085
}
10891086

10901087
return region.trim()

apps/cli/mcp/README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -85,7 +85,7 @@ Note: if you are running Daytona MCP Server on Windows OS, add the following to
8585

8686
- Parameters:
8787
- `id` (optional): Sandbox ID - if provided, an existing sandbox will be used, new one will be created otherwise
88-
- `target` (default: "us"): Target region of the sandbox
88+
- `target` (optional): Target region of the sandbox (if not provided, default region of the organization is used)
8989
- `image`: Image of the sandbox (optional)
9090
- `auto_stop_interval` (default: "15"): Auto-stop interval in minutes (0 means disabled)
9191
- `auto_archive_interval` (default: "10080"): Auto-archive interval in minutes (0 means the maximum interval will be used)

0 commit comments

Comments
 (0)