Versión: 1.0
Fecha: 27 de Diciembre, 2025
Estado: OPERATIVO - Procedimiento Estándar
Arquitecto: Carlos Alberto Torres Camargo
Este documento describe el proceso operativo completo para configurar un nuevo tenant (cliente) en Nebula ERP.
Duración Estimada: 2-4 horas por tenant
Responsable: DevOps + Arquitecto
Prerequisitos: Credenciales de bases de datos, valores de configuración del cliente
Roles Requeridos:
Pre-requisitos:
-- Conectar como superusuario postgres
psql -U postgres -h ${DB_HOST}
-- Crear base de datos del tenant
CREATE DATABASE nebula_${TENANT_NAME}_${ENVIRONMENT}
WITH
OWNER = nebula_admin
ENCODING = 'UTF8'
LC_COLLATE = 'en_US.UTF-8'
LC_CTYPE = 'en_US.UTF-8'
TABLESPACE = pg_default
CONNECTION LIMIT = 100;
-- Crear usuario específico del tenant (opcional pero recomendado)
CREATE USER nebula_${TENANT_NAME} WITH ENCRYPTED PASSWORD '${SECURE_PASSWORD}';
-- Otorgar permisos
GRANT ALL PRIVILEGES ON DATABASE nebula_${TENANT_NAME}_${ENVIRONMENT}
TO nebula_${TENANT_NAME};
-- Conectar a la nueva BD
\c nebula_${TENANT_NAME}_${ENVIRONMENT}
-- Crear schemas (si se requieren schemas adicionales)
CREATE SCHEMA IF NOT EXISTS contabilidad AUTHORIZATION nebula_${TENANT_NAME};
CREATE SCHEMA IF NOT EXISTS facturacion AUTHORIZATION nebula_${TENANT_NAME};
CREATE SCHEMA IF NOT EXISTS nomina AUTHORIZATION nebula_${TENANT_NAME};
CREATE SCHEMA IF NOT EXISTS inventario AUTHORIZATION nebula_${TENANT_NAME};
-- Otorgar permisos en schemas
GRANT ALL ON SCHEMA contabilidad TO nebula_${TENANT_NAME};
GRANT ALL ON SCHEMA facturacion TO nebula_${TENANT_NAME};
GRANT ALL ON SCHEMA nomina TO nebula_${TENANT_NAME};
GRANT ALL ON SCHEMA inventario TO nebula_${TENANT_NAME};
Ejemplo Real (Tenant: Acme Corp, Ambiente: Producción):
CREATE DATABASE nebula_acme_prod
WITH OWNER = nebula_admin ENCODING = 'UTF8';
CREATE USER nebula_acme WITH ENCRYPTED PASSWORD 'Acm3Pr0d$2025!Secure';
GRANT ALL PRIVILEGES ON DATABASE nebula_acme_prod TO nebula_acme;
-- Conectar como SYSDBA
connect sys/${SYS_PASSWORD}@${ORACLE_SID} as sysdba
-- Crear tablespace para el tenant
CREATE TABLESPACE nebula_${TENANT_NAME}_data
DATAFILE '/u01/oradata/${ORACLE_SID}/nebula_${TENANT_NAME}_data.dbf'
SIZE 1G
AUTOEXTEND ON NEXT 100M MAXSIZE UNLIMITED
SEGMENT SPACE MANAGEMENT AUTO;
-- Crear temporary tablespace
CREATE TEMPORARY TABLESPACE nebula_${TENANT_NAME}_temp
TEMPFILE '/u01/oradata/${ORACLE_SID}/nebula_${TENANT_NAME}_temp.dbf'
SIZE 500M
AUTOEXTEND ON NEXT 50M MAXSIZE UNLIMITED;
-- Crear usuario del tenant
CREATE USER nebula_${TENANT_NAME}
IDENTIFIED BY "${SECURE_PASSWORD}"
DEFAULT TABLESPACE nebula_${TENANT_NAME}_data
TEMPORARY TABLESPACE nebula_${TENANT_NAME}_temp
QUOTA UNLIMITED ON nebula_${TENANT_NAME}_data;
-- Otorgar permisos
GRANT CONNECT, RESOURCE, CREATE VIEW, CREATE MATERIALIZED VIEW TO nebula_${TENANT_NAME};
GRANT CREATE SESSION TO nebula_${TENANT_NAME};
-- Permisos adicionales si se requieren
GRANT CREATE SEQUENCE TO nebula_${TENANT_NAME};
GRANT CREATE TRIGGER TO nebula_${TENANT_NAME};
GRANT CREATE PROCEDURE TO nebula_${TENANT_NAME};
Ejemplo Real:
CREATE TABLESPACE nebula_acme_data
DATAFILE '/u01/oradata/ORCL/nebula_acme_data.dbf' SIZE 2G;
CREATE USER nebula_acme IDENTIFIED BY "Acm3Prod$2025!Oracle"
DEFAULT TABLESPACE nebula_acme_data
QUOTA UNLIMITED ON nebula_acme_data;
GRANT CONNECT, RESOURCE TO nebula_acme;
-- Conectar como sa o usuario con permisos de sysadmin
USE master;
GO
-- Crear base de datos del tenant
CREATE DATABASE nebula_${TENANT_NAME}_${ENVIRONMENT}
ON PRIMARY
(
NAME = N'nebula_${TENANT_NAME}_data',
FILENAME = N'D:\MSSQL\DATA\nebula_${TENANT_NAME}_data.mdf',
SIZE = 1024MB,
MAXSIZE = UNLIMITED,
FILEGROWTH = 256MB
)
LOG ON
(
NAME = N'nebula_${TENANT_NAME}_log',
FILENAME = N'D:\MSSQL\DATA\nebula_${TENANT_NAME}_log.ldf',
SIZE = 512MB,
MAXSIZE = 2GB,
FILEGROWTH = 128MB
);
GO
-- Crear login y usuario del tenant
CREATE LOGIN nebula_${TENANT_NAME}
WITH PASSWORD = '${SECURE_PASSWORD}';
GO
USE nebula_${TENANT_NAME}_${ENVIRONMENT};
GO
CREATE USER nebula_${TENANT_NAME} FOR LOGIN nebula_${TENANT_NAME};
GO
-- Otorgar permisos
ALTER ROLE db_owner ADD MEMBER nebula_${TENANT_NAME};
GO
-- Crear schemas (SQL Server soporta schemas)
CREATE SCHEMA contabilidad AUTHORIZATION nebula_${TENANT_NAME};
CREATE SCHEMA facturacion AUTHORIZATION nebula_${TENANT_NAME};
CREATE SCHEMA nomina AUTHORIZATION nebula_${TENANT_NAME};
CREATE SCHEMA inventario AUTHORIZATION nebula_${TENANT_NAME};
GO
Ejemplo Real:
CREATE DATABASE nebula_acme_prod;
GO
CREATE LOGIN nebula_acme WITH PASSWORD = 'Acm3Prod$2025!SQLServer';
GO
USE nebula_acme_prod;
CREATE USER nebula_acme FOR LOGIN nebula_acme;
ALTER ROLE db_owner ADD MEMBER nebula_acme;
GO
database_config_v2IMPORTANTE: NUNCA almacenar contraseñas en texto plano.
Opción A: Encriptar con Jasypt (Recomendado)
# Usar herramienta de encriptación Jasypt
java -cp jasypt-1.9.3.jar \
org.jasypt.intf.cli.JasyptPBEStringEncryptionCLI \
input="Acm3Prod$2025!Secure" \
password="${MASTER_ENCRYPTION_KEY}" \
algorithm=PBEWithMD5AndDES
# Output: ENC(QxT7yHj9kL...)
# Usar este valor encriptado en database_config_v2
Opción B: Variables de Entorno
# Almacenar en secret manager (AWS Secrets Manager, HashiCorp Vault)
# Referenciar como placeholder: ${ACME_DB_PASSWORD}
Endpoint: POST /api/v2/database-config/create
Request Body (PostgreSQL):
{
"name": "Acme Corp - Producción",
"code": "acme_prod",
"url": "jdbc:postgresql://${POSTGRES_HOST}:5432/nebula_acme_prod",
"username": "nebula_acme",
"password": "ENC(QxT7yHj9kL...)",
"database": "nebula_acme_prod",
"schema": "public",
"type": "POSTGRESQL",
"clientId": "acme",
"environment": "prod",
"maxPoolSize": 20,
"minIdle": 5,
"connectionTimeout": 30000,
"idleTimeout": 600000,
"maxLifetime": 1800000,
"driverClassName": "org.postgresql.Driver",
"hibernateDialect": "org.hibernate.dialect.PostgreSQL95Dialect",
"validationQuery": "SELECT 1",
"testOnBorrow": true,
"testWhileIdle": true
}
Request Body (Oracle):
{
"name": "Acme Corp - Producción Oracle",
"code": "acme_prod",
"url": "jdbc:oracle:thin:@${ORACLE_HOST}:1521:${ORACLE_SID}",
"username": "nebula_acme",
"password": "ENC(ZxW3mN2pQ...)",
"database": "${ORACLE_SID}",
"schema": "NEBULA_ACME",
"type": "ORACLE",
"clientId": "acme",
"environment": "prod",
"maxPoolSize": 25,
"minIdle": 10,
"connectionTimeout": 30000,
"driverClassName": "oracle.jdbc.OracleDriver",
"hibernateDialect": "org.hibernate.dialect.Oracle12cDialect",
"validationQuery": "SELECT 1 FROM DUAL"
}
Request Body (SQL Server):
{
"name": "Acme Corp - Producción SQL Server",
"code": "acme_prod",
"url": "jdbc:sqlserver://${SQLSERVER_HOST}:1433;databaseName=nebula_acme_prod",
"username": "nebula_acme",
"password": "ENC(MnB5kT8vL...)",
"database": "nebula_acme_prod",
"schema": "dbo",
"type": "SQLSERVER",
"clientId": "acme",
"environment": "prod",
"maxPoolSize": 20,
"driverClassName": "com.microsoft.sqlserver.jdbc.SQLServerDriver",
"hibernateDialect": "org.hibernate.dialect.SQLServerDialect",
"validationQuery": "SELECT 1"
}
curl -X POST "http://${SIMAPPE_ADMIN_HOST}/api/v2/database-config/create" \
-H "Authorization: Bearer ${ADMIN_JWT_TOKEN}" \
-H "Content-Type: application/json" \
-d @tenant_config.json
Validar Respuesta:
{
"success": true,
"data": {
"id": 123,
"code": "acme_prod",
"clientId": "acme",
"createdAt": "2025-01-15T10:30:00Z"
},
"message": "Database configuration created successfully"
}
Flyway CLI Instalado:
# Verificar instalación
flyway -v
# Flyway Community Edition 10.x
Migraciones SQL Preparadas:
src/main/resources/db/migration/
├── postgresql/
│ ├── V1__create_schema_contabilidad.sql
│ ├── V2__create_table_cuenta_contable.sql
│ └── V3__insert_plan_cuentas.sql
├── oracle/
│ └── [mismos archivos adaptados para Oracle]
└── sqlserver/
└── [mismos archivos adaptados para SQL Server]
# Configurar variables
export TENANT_DB_URL="jdbc:postgresql://${POSTGRES_HOST}:5432/nebula_acme_prod"
export TENANT_DB_USER="nebula_acme"
export TENANT_DB_PASSWORD="${SECURE_PASSWORD}"
# Ejecutar Flyway
flyway \
-url="${TENANT_DB_URL}" \
-user="${TENANT_DB_USER}" \
-password="${TENANT_DB_PASSWORD}" \
-locations="filesystem:./src/main/resources/db/migration/postgresql" \
-schemas="public,contabilidad,facturacion,nomina,inventario" \
-baselineOnMigrate=true \
migrate
Output Esperado:
Flyway Community Edition 10.x
Database: jdbc:postgresql://prod-db.acme.internal:5432/nebula_acme_prod (PostgreSQL 16.1)
Successfully validated 3 migrations (execution time 00:00.012s)
Current version of schema "public": << Empty Schema >>
Migrating schema "public" to version "1 - create schema contabilidad"
Migrating schema "public" to version "2 - create table cuenta contable"
Migrating schema "public" to version "3 - insert plan cuentas"
Successfully applied 3 migrations to schema "public", now at version v3 (execution time 00:01.234s)
export TENANT_DB_URL="jdbc:oracle:thin:@${ORACLE_HOST}:1521:ORCL"
export TENANT_DB_USER="nebula_acme"
export TENANT_DB_PASSWORD="${SECURE_PASSWORD}"
flyway \
-url="${TENANT_DB_URL}" \
-user="${TENANT_DB_USER}" \
-password="${TENANT_DB_PASSWORD}" \
-locations="filesystem:./src/main/resources/db/migration/oracle" \
-schemas="NEBULA_ACME" \
-oracle.sqlplus=true \
migrate
export TENANT_DB_URL="jdbc:sqlserver://${SQLSERVER_HOST}:1433;databaseName=nebula_acme_prod"
export TENANT_DB_USER="nebula_acme"
export TENANT_DB_PASSWORD="${SECURE_PASSWORD}"
flyway \
-url="${TENANT_DB_URL}" \
-user="${TENANT_DB_USER}" \
-password="${TENANT_DB_PASSWORD}" \
-locations="filesystem:./src/main/resources/db/migration/sqlserver" \
-schemas="dbo,contabilidad,facturacion,nomina,inventario" \
migrate
Endpoint: POST /api/v1/company/create (SimappeAdmin)
{
"name": "Acme Corp S.A.S.",
"legalName": "ACME CORPORATION SOCIEDAD POR ACCIONES SIMPLIFICADA",
"nit": "900123456-7",
"address": "Calle 50 #45-30",
"city": "Medellín",
"state": "Antioquia",
"country": "Colombia",
"phone": "+57 4 3001234",
"email": "contacto@acme.com.co",
"website": "https://acme.com.co",
"industry": "TECNOLOGIA",
"employees": 50,
"annualRevenue": 5000000000,
"fiscalYear": "CALENDAR",
"clientId": "acme"
}
Guardar companyId de la respuesta: Ejemplo companyId: 456
Endpoint: POST /api/v1/customer/create
{
"name": "Acme Corp Empresa Principal",
"legalName": "ACME CORPORATION S.A.S.",
"identificationType": "NIT",
"identificationNumber": "900123456-7",
"email": "admin@acme.com.co",
"phone": "+57 300 1234567",
"address": "Calle 50 #45-30, Medellín",
"clientId": "acme",
"active": true
}
Guardar customerId: Ejemplo customerId: 789
Endpoint: POST /api/v1/user/create
{
"username": "admin@acme.com.co",
"email": "admin@acme.com.co",
"password": "TempPassword123!",
"firstName": "Administrador",
"lastName": "Acme Corp",
"phone": "+57 300 1234567",
"clientId": "acme",
"companyId": 456,
"customerId": 789,
"roles": ["SUPER_ADMIN", "CONTADOR", "FACTURADOR"],
"active": true,
"mustChangePassword": true
}
Guardar userId: Ejemplo userId: 123
# Test con psql (PostgreSQL)
psql -h ${POSTGRES_HOST} -U nebula_acme -d nebula_acme_prod -c "SELECT COUNT(*) FROM flyway_schema_history;"
# Test con sqlplus (Oracle)
sqlplus nebula_acme/${PASSWORD}@${ORACLE_HOST}:1521/${ORACLE_SID}
SQL> SELECT COUNT(*) FROM FLYWAY_SCHEMA_HISTORY;
# Test con sqlcmd (SQL Server)
sqlcmd -S ${SQLSERVER_HOST} -d nebula_acme_prod -U nebula_acme -P ${PASSWORD}
1> SELECT COUNT(*) FROM flyway_schema_history;
2> GO
Request con JWT del usuario creado:
# 1. Obtener JWT
curl -X POST "http://${OAUTH2_SERVER_HOST}/oauth/token" \
-H "Content-Type: application/x-www-form-urlencoded" \
-d "grant_type=password&username=admin@acme.com.co&password=TempPassword123!&client_id=nebula-app"
# 2. Usar JWT para consultar endpoint
curl -X GET "http://${NEBULA_ACCOUNTING_HOST}/api/v1/accounting/cuentas/read" \
-H "Authorization: Bearer ${JWT_TOKEN}"
# Expected response: [] (lista vacía si no hay cuentas aún)
curl -X POST "http://${NEBULA_ACCOUNTING_HOST}/api/v1/accounting/cuentas/create" \
-H "Authorization: Bearer ${JWT_TOKEN}" \
-H "Content-Type: application/json" \
-d '{
"codigo": "1105",
"nombre": "CAJA GENERAL",
"tipo": "ACTIVO",
"naturaleza": "DEBITO",
"nivel": 4,
"activo": true
}'
# Expected response:
# {
# "success": true,
# "data": {
# "id": 1,
# "codigo": "1105",
# "nombre": "CAJA GENERAL",
# ...
# }
# }
Verificar en BD:
-- PostgreSQL
SELECT * FROM contabilidad.cuenta_contable WHERE codigo = '1105';
-- Oracle
SELECT * FROM NEBULA_ACME.cuenta_contable WHERE codigo = '1105';
-- SQL Server
SELECT * FROM contabilidad.cuenta_contable WHERE codigo = '1105';
Endpoint: POST /api/v1/menu-customization/create
{
"customizationLevel": "COMPANY",
"companyId": 456,
"menuId": 10,
"visible": true,
"order": 1,
"label": "Contabilidad",
"icon": "account_balance"
}
Endpoint: POST /api/v1/consecutive-configuration/create
{
"name": "Facturas de Venta",
"code": "FV",
"type": "FACTURA_VENTA",
"prefix": "FV-",
"suffix": "",
"padding": 6,
"startNumber": 1,
"currentNumber": 0,
"companyId": 456,
"autoReset": "YEARLY",
"validFrom": "2025-01-01",
"validUntil": "2025-12-31"
}
Endpoint: POST /api/v1/email-configuration/create
{
"configLevel": "COMPANY",
"companyId": 456,
"provider": "AWS_SES",
"fromEmail": "noreply@acme.com.co",
"fromName": "Acme Corp Notificaciones",
"smtpHost": "email-smtp.us-east-1.amazonaws.com",
"smtpPort": 587,
"username": "${AWS_SES_USERNAME}",
"password": "${AWS_SES_PASSWORD}",
"useTLS": true
}
Antes de entregar el tenant al cliente, validar:
database_config_v2 Registrado: Registro en SimappeAdmin creado-- PostgreSQL
DROP DATABASE IF EXISTS nebula_${TENANT_NAME}_${ENVIRONMENT};
DROP USER IF EXISTS nebula_${TENANT_NAME};
-- Oracle
DROP USER nebula_${TENANT_NAME} CASCADE;
DROP TABLESPACE nebula_${TENANT_NAME}_data INCLUDING CONTENTS AND DATAFILES;
-- SQL Server
USE master;
DROP DATABASE nebula_${TENANT_NAME}_${ENVIRONMENT};
DROP LOGIN nebula_${TENANT_NAME};
database_config_v2curl -X DELETE "http://${SIMAPPE_ADMIN_HOST}/api/v2/database-config/delete?id=123" \
-H "Authorization: Bearer ${ADMIN_JWT_TOKEN}"
Configuración de Tenant = 6 Pasos
database_config_v2 (SimappeAdmin API)Resultado: Tenant operativo y listo para uso en 2-4 horas.
Documento Operativo - Configuración de Tenant
© 2025 CatcSoft - Medellín, Colombia
| Version | Fecha | Autor | Descripcion |
|---|---|---|---|
| 1.1.0 | 2026-03-04 | Carlos Torres | Revision, sanitizacion y publicacion en Wiki Arquitectura Centrica. |
| 1.0.0 | 2025-12-27 | Carlos Torres | Creacion del documento. |