Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,15 @@ LOG_ENABLE_SESSION_LOGGER=false
# Enable HTTP request/response logging
LOG_ENABLE_HTTP_LOGGER=false

# Database Migration Configuration
# Run migrations automatically on startup (default: true)
# Set to false if you want to run migrations manually
#DB_MIGRATIONS_RUN=true
# Enable schema synchronization (default: false)
# WARNING: Only use DB_SYNCHRONIZE=true for development or fresh installs
# For production upgrades, always use migrations instead
#DB_SYNCHRONIZE=false

# TLS/HTTPS Configuration
# Enable built-in TLS support (set to 'true' to enable)
# When enabled, the service will serve HTTPS directly without needing a reverse proxy
Expand Down
8 changes: 7 additions & 1 deletion apps/backend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,13 @@
"test:debug": "vitest --inspect-brk --inspect --logHeapUsage --threads=false",
"test:e2e": "vitest run --coverage --config ./test/vitest.config.ts",
"test:e2e:watch": "vitest --coverage --config ./test/vitest.config.ts",
"registrar": "npx @hey-api/openapi-ts -i https://sandbox.eudi-wallet.org/api/api-json -o ./src/registrar/generated -c @hey-api/client-fetch"
"registrar": "npx @hey-api/openapi-ts -i https://sandbox.eudi-wallet.org/api/api-json -o ./src/registrar/generated -c @hey-api/client-fetch",
"typeorm": "tsx ./node_modules/typeorm/cli.js -d ./src/database/data-source.ts",
"migration:generate": "pnpm typeorm migration:generate ./src/database/migrations/$npm_config_name",
"migration:create": "pnpm typeorm migration:create ./src/database/migrations/$npm_config_name",
"migration:run": "pnpm typeorm migration:run",
"migration:revert": "pnpm typeorm migration:revert",
"migration:show": "pnpm typeorm migration:show"
},
"dependencies": {
"@animo-id/mdoc": "0.6.0-alpha-20260202121208",
Expand Down
41 changes: 41 additions & 0 deletions apps/backend/src/database/data-source.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import { join } from "node:path";
import { config } from "dotenv";
import { DataSource, DataSourceOptions } from "typeorm";

// Load environment variables
config({ path: join(__dirname, "..", "..", ".env") });
config({ path: join(__dirname, "..", "..", "..", "..", ".env") });

const dbType = process.env.DB_TYPE as "sqlite" | "postgres" | undefined;

const commonOptions: Partial<DataSourceOptions> = {
synchronize: false,
logging: process.env.DB_LOGGING === "true",
migrations: [join(__dirname, "migrations", "*.{ts,js}")],
migrationsTableName: "typeorm_migrations",
// Entity patterns - TypeORM CLI needs explicit patterns
entities: [join(__dirname, "..", "**", "*.entity.{ts,js}")],
};

let dataSourceOptions: DataSourceOptions;

if (dbType === "postgres") {
dataSourceOptions = {
type: "postgres",
host: process.env.DB_HOST || "localhost",
port: Number.parseInt(process.env.DB_PORT || "5432", 10),
username: process.env.DB_USERNAME || "postgres",
password: process.env.DB_PASSWORD || "postgres",
database: process.env.DB_DATABASE || "eudiplo",
...commonOptions,
} as DataSourceOptions;
} else {
const folder = process.env.FOLDER || "./assets";
dataSourceOptions = {
type: "sqlite",
database: join(folder, "service.db"),
...commonOptions,
} as DataSourceOptions;
}

export const AppDataSource = new DataSource(dataSourceOptions);
10 changes: 10 additions & 0 deletions apps/backend/src/database/database-validation.schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,4 +46,14 @@ export const DB_VALIDATION_SCHEMA = Joi.object({
})
.description("Database name")
.meta({ group: "database", order: 50 }),
DB_SYNCHRONIZE: Joi.boolean()
.default(true)
.description(
"Enable TypeORM schema synchronization. Set to false in production after initial setup and rely on migrations instead.",
)
.meta({ group: "database", order: 60 }),
DB_MIGRATIONS_RUN: Joi.boolean()
.default(true)
.description("Run pending database migrations automatically on startup")
.meta({ group: "database", order: 70 }),
});
33 changes: 30 additions & 3 deletions apps/backend/src/database/database.module.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
import { Module } from "@nestjs/common";
import { Logger, Module, OnModuleInit } from "@nestjs/common";
import { ConfigModule, ConfigService } from "@nestjs/config";
import { TypeOrmModule, TypeOrmModuleOptions } from "@nestjs/typeorm";
import { join } from "path";
import { DataSource } from "typeorm";
import * as migrations from "./migrations";

@Module({
imports: [
Expand All @@ -15,9 +17,21 @@ import { join } from "path";
"DB_TYPE",
);

// Default synchronize to false for production safety
// Use DB_SYNCHRONIZE=true only for development or fresh installs
const synchronize =
configService.getOrThrow<boolean>("DB_SYNCHRONIZE");

// Migrations are enabled by default, disable with DB_MIGRATIONS_RUN=false
const migrationsRun =
configService.getOrThrow<boolean>("DB_MIGRATIONS_RUN");

const commonOptions = {
synchronize: true,
synchronize,
autoLoadEntities: true,
migrationsRun,
migrations: Object.values(migrations),
migrationsTableName: "typeorm_migrations",
};

if (dbType === "postgres") {
Expand Down Expand Up @@ -47,4 +61,17 @@ import { join } from "path";
}),
],
})
export class DatabaseModule {}
export class DatabaseModule implements OnModuleInit {
private readonly logger = new Logger(DatabaseModule.name);

constructor(private readonly dataSource: DataSource) {}

async onModuleInit(): Promise<void> {
const pendingMigrations = await this.dataSource.showMigrations();
if (pendingMigrations) {
this.logger.warn(
"There are pending migrations. Run migrations to apply schema changes.",
);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import { MigrationInterface, QueryRunner } from "typeorm";

/**
* Baseline Migration - v2.0.0
*
* This migration marks the starting point for TypeORM migrations.
* It does NOT create tables - instead it relies on either:
*
* 1. **Existing installations**: Schema already exists from previous
* `synchronize: true` mode. This migration just records itself
* in the migration history.
*
* 2. **New installations**: Users should set `DB_SYNCHRONIZE=true`
* for the initial run to create the schema, then disable it.
*
* Future schema changes will be handled by generated migrations.
*/
export class BaselineMigration1740000000000 implements MigrationInterface {
name = "BaselineMigration1740000000000";

public async up(queryRunner: QueryRunner): Promise<void> {
// Check if schema already exists (existing installation)
const tables = await queryRunner.getTables(["tenant_entity"]);

if (tables.length > 0) {
console.log(
"[Migration] Existing database detected. Marking baseline as complete.",
);
return;
}

// New installation without schema
console.log(
"[Migration] Fresh database detected. Schema will be created by TypeORM synchronize.",
);
console.log(
"[Migration] Ensure DB_SYNCHRONIZE=true is set for initial setup.",
);
}

public async down(): Promise<void> {
// This is a baseline marker - nothing to revert
console.log("[Migration] Baseline migration has nothing to revert.");
}
}
34 changes: 34 additions & 0 deletions apps/backend/src/database/migrations/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
# TypeORM Migrations

This directory contains TypeORM database migrations for the EUDIPLO backend.

## Migration Commands

Run these commands from the `apps/backend` directory:

```bash
# Generate a new migration based on entity changes
pnpm migration:generate --name=MigrationName

# Create an empty migration
pnpm migration:create --name=MigrationName

# Run pending migrations
pnpm migration:run

# Revert the last migration
pnpm migration:revert

# Show migration status
pnpm migration:show
```

## Important Notes

1. **Production Safety**: The `synchronize` option is now disabled by default. Use migrations for schema changes in production.

2. **For Existing Databases**: If you're upgrading from a version that used `synchronize: true`, your database schema should already match the entities. The initial migration will be skipped automatically if the tables already exist.

3. **For New Installations**: Migrations will run automatically on startup (unless `DB_MIGRATIONS_RUN=false`).

4. **Development Mode**: You can enable `DB_SYNCHRONIZE=true` for development to auto-sync schema changes, but this is not recommended for production.
6 changes: 6 additions & 0 deletions apps/backend/src/database/migrations/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
/**
* Migration index - exports all migration classes for TypeORM.
* Import migrations directly to avoid ESM/CJS compatibility issues
* with dynamic file loading during tests.
*/
export { BaselineMigration1740000000000 } from "./1740000000000-BaselineMigration";
3 changes: 3 additions & 0 deletions apps/backend/test/vitest.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,9 @@ export default defineConfig({
AUTH_CLIENT_SECRET: "e2e-test-secret",
ENCRYPTION_KEY:
"0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef",
// Use synchronize for tests (fresh DB each run), skip migrations
DB_SYNCHRONIZE: "true",
DB_MIGRATIONS_RUN: "false",
},
},
plugins: [
Expand Down
6 changes: 0 additions & 6 deletions assets/config/root/issuance/credentials/pid.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,12 +20,6 @@
}
]
},
"claimsWebhook": {
"url": "http://localhost:8787/claims",
"auth": {
"type": "none"
}
},
"claims": {
"issuing_country": "DE",
"issuing_authority": "DE",
Expand Down
7 changes: 7 additions & 0 deletions deployment/docker-compose/.env.full.example
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,13 @@ DB_USERNAME=eudiplo
DB_PASSWORD=changeme-very-strong-password
DB_DATABASE=eudiplo

# Database Migrations
# Run migrations automatically on startup (default: true)
#DB_MIGRATIONS_RUN=true
# Enable schema synchronization (default: false)
# WARNING: Only use for development, use migrations in production
#DB_SYNCHRONIZE=false

# =============================================================================
# Key Management: HashiCorp Vault
# ⚠️ For production, use a properly configured Vault instance
Expand Down
7 changes: 7 additions & 0 deletions deployment/docker-compose/.env.minimal.example
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,13 @@ DB_TYPE=sqlite
# SQLite database file location (inside container)
# DB_DATABASE=./data/eudiplo.sqlite

# Database Migrations
# Run migrations automatically on startup (default: true)
#DB_MIGRATIONS_RUN=true
# Enable schema synchronization (default: false)
# For dev with SQLite, you can use DB_SYNCHRONIZE=true for convenience
#DB_SYNCHRONIZE=false

# =============================================================================
# Key Management: Database-backed (default)
# =============================================================================
Expand Down
7 changes: 7 additions & 0 deletions deployment/docker-compose/.env.standard.example
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,13 @@ DB_USERNAME=eudiplo
DB_PASSWORD=changeme-strong-password
DB_DATABASE=eudiplo

# Database Migrations
# Run migrations automatically on startup (default: true)
#DB_MIGRATIONS_RUN=true
# Enable schema synchronization (default: false)
# WARNING: Only use for development, use migrations in production
#DB_SYNCHRONIZE=false

# =============================================================================
# Key Management: Database-backed
# =============================================================================
Expand Down
56 changes: 56 additions & 0 deletions docs/architecture/database.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,62 @@ the service.

---

## Database Migrations

Starting from v2.0.0, EUDIPLO uses TypeORM migrations for database schema management instead of automatic synchronization. This provides:

- **Safe upgrades**: Schema changes are applied in a controlled manner
- **Rollback capability**: Migrations can be reverted if needed
- **Production safety**: No accidental schema changes in production

### How Migrations Work

By default, migrations run automatically when the service starts (`DB_MIGRATIONS_RUN=true`). The migration system detects whether you're running a fresh installation or upgrading:

- **Fresh installation**: The baseline migration creates all tables from scratch
- **Upgrading**: If tables already exist (from previous `synchronize: true` mode), the baseline migration is skipped, and only new migrations are applied

### Configuration

| Variable | Default | Description |
| ------------------- | ------- | --------------------------------- |
| `DB_MIGRATIONS_RUN` | `true` | Run pending migrations on startup |
| `DB_SYNCHRONIZE` | `false` | Auto-sync schema (⚠️ dev only) |

!!! warning "Production Safety"

Never set `DB_SYNCHRONIZE=true` in production. Use migrations instead to ensure controlled schema updates.

### Manual Migration Commands

From the `apps/backend` directory:

```bash
# Generate a new migration based on entity changes
pnpm migration:generate --name=AddNewFeature

# Run pending migrations
pnpm migration:run

# Revert the last migration
pnpm migration:revert

# Show migration status
pnpm migration:show
```

### Upgrading from Pre-2.0.0

If you're upgrading from a version that used `synchronize: true`:

1. Your existing database schema is already correct
2. The baseline migration will detect existing tables and skip creation
3. Your data is preserved, and the migration history starts fresh

No action is required — just start the new version, and migrations will handle the rest.

---

## Encryption at Rest

EUDIPLO encrypts sensitive data at rest using **AES-256-GCM** authenticated encryption. This protects cryptographic keys and personal information stored in the database from unauthorized access, even if the database itself is compromised.
Expand Down
2 changes: 2 additions & 0 deletions docs/generated/config-database.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,5 @@
| `DB_USERNAME` | `string` | Database username [when DB_TYPE is {"override":true} \| "sqlite" → otherwise required] |
| `DB_PASSWORD` | `string` | Database password [when DB_TYPE is {"override":true} \| "sqlite" → otherwise required] |
| `DB_DATABASE` | `string` | Database name [when DB_TYPE is {"override":true} \| "sqlite" → otherwise required] |
| `DB_SYNCHRONIZE` | `boolean` | Enable TypeORM schema synchronization. WARNING: Only use for development, never in production. (default: `false`) |
| `DB_MIGRATIONS_RUN` | `boolean` | Run pending database migrations automatically on startup (default: `true`) |
2 changes: 1 addition & 1 deletion tsconfig.scripts.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,4 +10,4 @@
"include": [
"scripts/**/*.ts"
]
}
}
Loading