or run

npx @tessl/cli init
Log in

Version

Tile

Overview

Evals

Files

Files

docs

blob-granules.mdc-api.mdgo-api.mdindex.mdjava-api.mdkey-encoding.mdmulti-tenancy.mdpython-api.mdruby-api.md

multi-tenancy.mddocs/

0

# Multi-Tenancy Guide

1

2

FoundationDB provides built-in multi-tenancy support, allowing multiple applications or users to share the same database cluster while maintaining complete data isolation. Each tenant gets its own isolated keyspace that is invisible to other tenants.

3

4

## Core Concepts

5

6

- **Tenant**: An isolated keyspace within a FoundationDB cluster

7

- **Tenant Name**: Unique identifier for a tenant (byte array)

8

- **Tenant Transaction**: Transaction scoped to a specific tenant's keyspace

9

- **Data Isolation**: Complete separation of data between tenants

10

- **Shared Infrastructure**: All tenants share the same cluster resources

11

12

## Capabilities

13

14

### Administrative Tenant Management

15

16

Functions for creating, deleting, and listing tenants in the cluster. These are administrative operations that manage tenant metadata.

17

18

```python { .api }

19

# Python API - Administrative functions (fdb.tenant_management module)

20

def create_tenant(db_or_tr, tenant_name: bytes) -> None:

21

"""

22

Create a new tenant in the cluster.

23

24

Args:

25

db_or_tr: Database or Transaction handle

26

tenant_name: Unique name for the tenant

27

28

Raises:

29

FDBError(2132): tenant_already_exists if tenant exists

30

"""

31

32

def delete_tenant(db_or_tr, tenant_name: bytes) -> None:

33

"""

34

Delete a tenant from the cluster.

35

36

Args:

37

db_or_tr: Database or Transaction handle

38

tenant_name: Name of tenant to delete

39

40

Raises:

41

FDBError(2131): tenant_not_found if tenant doesn't exist

42

"""

43

44

def list_tenants(db_or_tr, begin: bytes, end: bytes, limit: int) -> FDBTenantList:

45

"""

46

List tenants in the cluster within the specified range.

47

48

Args:

49

db_or_tr: Database or Transaction handle

50

begin: Start of tenant name range (inclusive)

51

end: End of tenant name range (exclusive)

52

limit: Maximum number of tenants to return

53

54

Returns:

55

Iterable of KeyValue objects with tenant names and metadata

56

"""

57

58

class FDBTenantList:

59

"""Iterator for tenant listing results."""

60

def to_list(self) -> List[KeyValue]: ...

61

def __iter__(self) -> Iterator[KeyValue]: ...

62

```

63

64

```c { .api }

65

// C API - Administrative tenant operations

66

DLLEXPORT WARN_UNUSED_RESULT fdb_error_t fdb_database_open_tenant(FDBDatabase* d,

67

uint8_t const* tenant_name,

68

int tenant_name_length,

69

FDBTenant** out_tenant);

70

71

DLLEXPORT WARN_UNUSED_RESULT fdb_error_t fdb_tenant_create_transaction(FDBTenant* tenant,

72

FDBTransaction** out_transaction);

73

74

DLLEXPORT WARN_UNUSED_RESULT FDBFuture* fdb_tenant_get_id(FDBTenant* tenant);

75

76

DLLEXPORT void fdb_tenant_destroy(FDBTenant* tenant);

77

```

78

79

### Tenant Operations and Management

80

81

```python { .api }

82

# Python API

83

class Database:

84

def open_tenant(self, tenant_name: bytes) -> Tenant:

85

"""

86

Open tenant handle for multi-tenancy operations.

87

88

Args:

89

tenant_name: Unique name for the tenant

90

91

Returns:

92

Tenant handle with isolated keyspace

93

"""

94

95

class Tenant:

96

def create_transaction(self) -> Transaction:

97

"""

98

Create transaction within this tenant's keyspace.

99

All operations are automatically scoped to tenant.

100

101

Returns:

102

Transaction handle scoped to tenant

103

"""

104

```

105

106

```java { .api }

107

// Java API

108

public interface Database {

109

Tenant openTenant(byte[] tenantName);

110

}

111

112

public interface Tenant extends Transactor, ReadTransactor {

113

Transaction createTransaction();

114

byte[] getName();

115

}

116

```

117

118

```go { .api }

119

// Go API

120

type Database interface {

121

OpenTenant(tenantName []byte) (Tenant, error)

122

}

123

124

type Tenant interface {

125

CreateTransaction() (Transaction, error)

126

Transact(func(Transaction) (interface{}, error)) (interface{}, error)

127

ReadTransact(func(ReadTransaction) (interface{}, error)) (interface{}, error)

128

}

129

```

130

131

```ruby { .api }

132

# Ruby API

133

class FDB::Database

134

def open_tenant(tenant_name)

135

end

136

137

class FDB::Tenant

138

def create_transaction

139

def transact(&block)

140

def name

141

end

142

```

143

144

### Cross-Language Usage Patterns

145

146

**Python Example:**

147

148

```python

149

import fdb

150

151

fdb.api_version(740)

152

db = fdb.open()

153

154

# Open tenant

155

app1_tenant = db.open_tenant(b"app1")

156

app2_tenant = db.open_tenant(b"app2")

157

158

@fdb.transactional

159

def store_user_data(tenant, user_id, data):

160

tr = tenant.create_transaction()

161

tr.set(f"user:{user_id}".encode(), data.encode())

162

163

# Data is isolated per tenant

164

store_user_data(app1_tenant, "user123", "app1_data")

165

store_user_data(app2_tenant, "user123", "app2_data") # Different keyspace

166

```

167

168

**Java Example:**

169

170

```java

171

FDB fdb = FDB.selectAPIVersion(740);

172

Database db = fdb.open();

173

174

// Open tenants

175

Tenant app1Tenant = db.openTenant("app1".getBytes());

176

Tenant app2Tenant = db.openTenant("app2".getBytes());

177

178

// Each tenant has isolated data

179

String app1Result = app1Tenant.run(tr -> {

180

tr.set("user:123".getBytes(), "app1_data".getBytes());

181

return "stored";

182

});

183

184

String app2Result = app2Tenant.run(tr -> {

185

tr.set("user:123".getBytes(), "app2_data".getBytes());

186

return "stored";

187

});

188

```

189

190

**Go Example:**

191

192

```go

193

fdb.APIVersion(740)

194

fdb.StartNetwork()

195

defer fdb.StopNetwork()

196

197

db, _ := fdb.OpenDatabase("")

198

199

// Open tenants

200

app1Tenant, _ := db.OpenTenant([]byte("app1"))

201

app2Tenant, _ := db.OpenTenant([]byte("app2"))

202

203

// Isolated operations

204

app1Tenant.Transact(func(tr fdb.Transaction) (interface{}, error) {

205

tr.Set(fdb.Key("user:123"), []byte("app1_data"))

206

return nil, nil

207

})

208

209

app2Tenant.Transact(func(tr fdb.Transaction) (interface{}, error) {

210

tr.Set(fdb.Key("user:123"), []byte("app2_data"))

211

return nil, nil

212

})

213

```

214

215

**Ruby Example:**

216

217

```ruby

218

FDB.api_version(740)

219

db = FDB.open

220

221

# Open tenants

222

app1_tenant = db.open_tenant("app1")

223

app2_tenant = db.open_tenant("app2")

224

225

# Isolated transactions

226

app1_tenant.transact do |tr|

227

tr["user:123"] = "app1_data"

228

end

229

230

app2_tenant.transact do |tr|

231

tr["user:123"] = "app2_data" # Completely separate from app1

232

end

233

```

234

235

## Multi-Tenant Application Patterns

236

237

### Tenant-per-Customer Pattern

238

239

```python

240

class MultiTenantService:

241

def __init__(self, database):

242

self.db = database

243

self.tenant_cache = {}

244

245

def get_tenant(self, customer_id):

246

if customer_id not in self.tenant_cache:

247

tenant_name = f"customer_{customer_id}".encode()

248

self.tenant_cache[customer_id] = self.db.open_tenant(tenant_name)

249

return self.tenant_cache[customer_id]

250

251

@fdb.transactional

252

def store_customer_data(self, customer_id, key, value):

253

tenant = self.get_tenant(customer_id)

254

tr = tenant.create_transaction()

255

tr.set(key.encode(), value.encode())

256

257

@fdb.transactional

258

def get_customer_data(self, customer_id, key):

259

tenant = self.get_tenant(customer_id)

260

tr = tenant.create_transaction()

261

return tr.get(key.encode()).wait()

262

```

263

264

### Tenant-per-Application Pattern

265

266

```java

267

public class ApplicationTenantManager {

268

private final Database db;

269

private final Map<String, Tenant> tenants = new ConcurrentHashMap<>();

270

271

public ApplicationTenantManager(Database db) {

272

this.db = db;

273

}

274

275

public Tenant getTenant(String appName) {

276

return tenants.computeIfAbsent(appName, name ->

277

db.openTenant(name.getBytes()));

278

}

279

280

public void storeAppData(String appName, String key, String value) {

281

Tenant tenant = getTenant(appName);

282

tenant.run(tr -> {

283

tr.set(key.getBytes(), value.getBytes());

284

return null;

285

});

286

}

287

288

public String getAppData(String appName, String key) {

289

Tenant tenant = getTenant(appName);

290

return tenant.read(tr -> {

291

byte[] value = tr.get(key.getBytes()).join();

292

return value != null ? new String(value) : null;

293

});

294

}

295

}

296

```

297

298

### Environment-based Tenancy

299

300

```go

301

type EnvironmentManager struct {

302

db fdb.Database

303

tenants map[string]fdb.Tenant

304

mutex sync.RWMutex

305

}

306

307

func NewEnvironmentManager(db fdb.Database) *EnvironmentManager {

308

return &EnvironmentManager{

309

db: db,

310

tenants: make(map[string]fdb.Tenant),

311

}

312

}

313

314

func (em *EnvironmentManager) GetTenant(environment string) (fdb.Tenant, error) {

315

em.mutex.RLock()

316

tenant, exists := em.tenants[environment]

317

em.mutex.RUnlock()

318

319

if exists {

320

return tenant, nil

321

}

322

323

em.mutex.Lock()

324

defer em.mutex.Unlock()

325

326

// Double-check after acquiring write lock

327

if tenant, exists := em.tenants[environment]; exists {

328

return tenant, nil

329

}

330

331

tenant, err := em.db.OpenTenant([]byte(environment))

332

if err != nil {

333

return nil, err

334

}

335

336

em.tenants[environment] = tenant

337

return tenant, nil

338

}

339

340

func (em *EnvironmentManager) StoreData(environment, key, value string) error {

341

tenant, err := em.GetTenant(environment)

342

if err != nil {

343

return err

344

}

345

346

_, err = tenant.Transact(func(tr fdb.Transaction) (interface{}, error) {

347

tr.Set(fdb.Key(key), []byte(value))

348

return nil, nil

349

})

350

351

return err

352

}

353

```

354

355

### Dynamic Tenant Management

356

357

```ruby

358

class DynamicTenantManager

359

def initialize(database)

360

@db = database

361

@tenant_registry = {}

362

@mutex = Mutex.new

363

end

364

365

def register_tenant(tenant_id, metadata = {})

366

@mutex.synchronize do

367

tenant_name = "tenant_#{tenant_id}"

368

369

@tenant_registry[tenant_id] = {

370

name: tenant_name,

371

tenant: @db.open_tenant(tenant_name),

372

metadata: metadata,

373

created_at: Time.now

374

}

375

end

376

end

377

378

def get_tenant(tenant_id)

379

@mutex.synchronize do

380

entry = @tenant_registry[tenant_id]

381

entry&.fetch(:tenant)

382

end

383

end

384

385

def execute_for_tenant(tenant_id, &block)

386

tenant = get_tenant(tenant_id)

387

raise "Tenant not found: #{tenant_id}" unless tenant

388

389

tenant.transact(&block)

390

end

391

392

def list_tenants

393

@mutex.synchronize do

394

@tenant_registry.map do |id, entry|

395

{

396

id: id,

397

name: entry[:name],

398

metadata: entry[:metadata],

399

created_at: entry[:created_at]

400

}

401

end

402

end

403

end

404

405

def tenant_statistics(tenant_id)

406

execute_for_tenant(tenant_id) do |tr|

407

# Get basic statistics about tenant data

408

count = tr.get_range('', "\xFF").count

409

{

410

tenant_id: tenant_id,

411

key_count: count,

412

sampled_at: Time.now

413

}

414

end

415

end

416

end

417

```

418

419

## Best Practices

420

421

### Tenant Naming Conventions

422

423

- Use consistent, predictable naming patterns

424

- Include version or environment information when needed

425

- Avoid special characters that might cause issues

426

- Keep names reasonably short but descriptive

427

428

```python

429

# Good tenant naming patterns

430

tenant_name = f"app_{app_name}_v{version}".encode()

431

tenant_name = f"customer_{customer_id}".encode()

432

tenant_name = f"env_{environment}_{service}".encode()

433

434

# Avoid

435

tenant_name = user_input.encode() # Unpredictable

436

tenant_name = f"a_very_long_tenant_name_that_contains_lots_of_information".encode()

437

```

438

439

### Tenant Lifecycle Management

440

441

```python

442

class TenantLifecycleManager:

443

def __init__(self, database):

444

self.db = database

445

self.active_tenants = set()

446

447

def provision_tenant(self, tenant_id, config=None):

448

tenant_name = f"tenant_{tenant_id}".encode()

449

tenant = self.db.open_tenant(tenant_name)

450

451

# Initialize tenant with default data

452

self._initialize_tenant_schema(tenant, config)

453

454

self.active_tenants.add(tenant_id)

455

return tenant

456

457

def _initialize_tenant_schema(self, tenant, config):

458

@fdb.transactional

459

def setup(tr):

460

tenant_tr = tenant.create_transaction()

461

462

# Set up initial configuration

463

tenant_tr.set(b'_config', json.dumps(config or {}).encode())

464

tenant_tr.set(b'_created_at', str(time.time()).encode())

465

tenant_tr.set(b'_version', b'1.0')

466

467

setup(self.db)

468

469

def deprovision_tenant(self, tenant_id):

470

# Note: Actual tenant deletion requires administrative operations

471

# This just removes from active set

472

self.active_tenants.discard(tenant_id)

473

474

def migrate_tenant_data(self, from_tenant_id, to_tenant_id):

475

from_tenant = self.db.open_tenant(f"tenant_{from_tenant_id}".encode())

476

to_tenant = self.db.open_tenant(f"tenant_{to_tenant_id}".encode())

477

478

@fdb.transactional

479

def migrate(tr):

480

from_tr = from_tenant.create_transaction()

481

to_tr = to_tenant.create_transaction()

482

483

# Copy all data from source to destination

484

for kv in from_tr.get_range(b'', b'\xFF'):

485

to_tr.set(kv.key, kv.value)

486

487

migrate(self.db)

488

```

489

490

### Error Handling in Multi-Tenant Applications

491

492

```java

493

public class TenantErrorHandler {

494

public <T> T executeWithTenant(String tenantId, Function<Tenant, T> operation) {

495

try {

496

Tenant tenant = getTenant(tenantId);

497

return operation.apply(tenant);

498

} catch (FDBException e) {

499

if (isTenantNotFound(e)) {

500

throw new TenantNotFoundException("Tenant not found: " + tenantId);

501

} else if (isTenantInaccessible(e)) {

502

throw new TenantInaccessibleException("Tenant temporarily unavailable: " + tenantId);

503

} else {

504

throw new TenantOperationException("Operation failed for tenant: " + tenantId, e);

505

}

506

}

507

}

508

509

private boolean isTenantNotFound(FDBException e) {

510

// Check specific error codes for tenant not found

511

return e.getCode() == 2131; // tenant_not_found

512

}

513

514

private boolean isTenantInaccessible(FDBException e) {

515

// Check for temporary tenant issues

516

return e.getCode() == 2132; // tenant_not_accessible

517

}

518

}

519

```

520

521

## Security Considerations

522

523

### Tenant Isolation Verification

524

525

```python

526

def verify_tenant_isolation():

527

"""Verify that tenants cannot access each other's data."""

528

529

tenant_a = db.open_tenant(b"tenant_a")

530

tenant_b = db.open_tenant(b"tenant_b")

531

532

# Store data in tenant A

533

@fdb.transactional

534

def store_in_a(tr):

535

tenant_tr = tenant_a.create_transaction()

536

tenant_tr.set(b'secret_key', b'tenant_a_secret')

537

538

store_in_a(db)

539

540

# Try to read from tenant B (should not see tenant A's data)

541

@fdb.transactional

542

def read_from_b(tr):

543

tenant_tr = tenant_b.create_transaction()

544

value = tenant_tr.get(b'secret_key').wait()

545

assert value is None, "Tenant isolation violated!"

546

547

# Also verify tenant B can't see tenant A's keyspace

548

count = 0

549

for kv in tenant_tr.get_range(b'', b'\xFF'):

550

count += 1

551

552

assert count == 0, "Tenant B should not see any keys from tenant A"

553

554

read_from_b(db)

555

```

556

557

### Tenant Access Control

558

559

```ruby

560

class TenantAccessController

561

def initialize(database)

562

@db = database

563

@access_policies = {}

564

end

565

566

def define_policy(tenant_id, user_id, permissions)

567

@access_policies["#{tenant_id}:#{user_id}"] = permissions

568

end

569

570

def check_access(tenant_id, user_id, operation)

571

policy_key = "#{tenant_id}:#{user_id}"

572

permissions = @access_policies[policy_key] || []

573

574

unless permissions.include?(operation)

575

raise "Access denied: #{user_id} cannot #{operation} on tenant #{tenant_id}"

576

end

577

end

578

579

def execute_with_access_check(tenant_id, user_id, operation, &block)

580

check_access(tenant_id, user_id, operation)

581

582

tenant = @db.open_tenant("tenant_#{tenant_id}")

583

tenant.transact(&block)

584

end

585

end

586

```

587

588

Multi-tenancy in FoundationDB provides robust isolation while maintaining high performance and operational simplicity. Each tenant operates as if it has its own dedicated database while sharing the underlying infrastructure efficiently.