Versión: 1.0
Fecha: 27 de Diciembre, 2025
Estado: NORMATIVO - Arquitectura de 2 Capas
Arquitecto: Carlos Alberto Torres Camargo
Nebula ERP NO se construye desde cero. Se construye SOBRE el Ecosistema Simappe, un stack empresarial completo diseñado para acelerar el desarrollo de aplicaciones multi-tenant escalables.
┌─────────────────────────────────────────────────────────┐
│ CAPA 2: NEBULA ERP (Negocio) │
│ ┌─────────────┬─────────────┬──────────────────────┐ │
│ │ Contabilidad│ Facturación │ Nómina │ Inventario │ │
│ │ CentricaModel (DTOs) │ CentricaShared (APIs) │ │
│ └─────────────┴─────────────┴──────────────────────┘ │
└─────────────────────────────────────────────────────────┘
▼
┌─────────────────────────────────────────────────────────┐
│ CAPA 1: SIMAPPE (Infraestructura Base) │
│ ┌──────────────────────────────────────────────────┐ │
│ │ Multi-Tenancy │ CRUD │ Security │ Notifications │ │
│ │ SimappeCommons (Framework) │ SimappeModel │ │
│ │ SimappeAdmin │ OAuth2 │ Gateway │ Archetype │ │
│ └──────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────┘
Principio Fundamental: Centrica construye SOLO la lógica de negocio (Capa 2). Toda la infraestructura (Capa 1) ya existe en Simappe.
| Componente | Versión | Uso Obligatorio |
|---|---|---|
| Java | 25 | Lenguaje |
| Spring Boot | 3.5.11 | Framework |
| Spring Cloud | 2025.0.0 | Microservicios |
| Maven | 3.9+ | Build |
| Lombok | Latest | Generación código |
| MapStruct | 1.6.3 | Mapeo DTO-Entity |
| Componente | Versión | Uso Obligatorio |
|---|---|---|
| Angular | 20 | Framework SPA |
| TypeScript | 5.x | Lenguaje |
| Signals | Built-in | Estado reactivo |
| RxJS | 7.x | Asincronía |
| Standalone Components | Obligatorio | NO usar NgModules |
SimappeAdmin (Sistema):
Nebula ERP (Negocio):
MongoDB (Auditoría):
Redis (Caché):
Cada tenant (cliente) tiene su PROPIA BASE DE DATOS FÍSICA. No comparten esquemas ni tablas.
database_config_v2 (SimappeAdmin)Ubicación: SimappeAdmin → Schema public → Tabla database_config_v2
Campos Clave:
@Data
public class DatabaseConfigV2Dto {
private Long id;
private String name; // "Centrica Producción"
private String code; // "centrica_prod"
private String url; // jdbc:oracle:thin:@db.centrica.internal:1521:NEBULA
private String username; // ${DB_USER}
private String password; // ${DB_PASSWORD} (encriptado)
private String database; // "nebula_prod"
private String schema; // "dbo" o "public"
private String type; // "ORACLE", "POSTGRESQL", "SQLSERVER"
private String clientId; // "centrica"
private String environment; // "prod"
// HikariCP Config
private Integer maxPoolSize = 10;
private Integer minIdle = 5;
private Long connectionTimeout = 30000L;
private String driverClassName; // "oracle.jdbc.OracleDriver"
private String hibernateDialect; // "org.hibernate.dialect.OracleDialect"
}
INSERT INTO database_config_v2 (
name, code, url, username, password,
database, schema, type, client_id, environment,
driver_class_name, hibernate_dialect, max_pool_size
) VALUES (
'Centrica Producción',
'centrica_prod',
'jdbc:oracle:thin:@${ORACLE_HOST}:${ORACLE_PORT}:${ORACLE_SID}',
'${CENTRICA_DB_USER}',
'${CENTRICA_DB_PASSWORD_ENCRYPTED}',
'NEBULA_PROD',
'CENTRICA',
'ORACLE',
'centrica',
'prod',
'oracle.jdbc.OracleDriver',
'org.hibernate.dialect.OracleDialect',
20
);
Variables de Entorno (NO hardcodear):
${ORACLE_HOST}: IP/DNS del servidor Oracle${ORACLE_PORT}: 1521 típicamente${ORACLE_SID} o ${ORACLE_SERVICE}: Identificador de instancia${CENTRICA_DB_USER}: Usuario con permisos en Oracle${CENTRICA_DB_PASSWORD_ENCRYPTED}: Contraseña cifrada (Jasypt/Vault)Paso 1: Request llega con JWT
GET /api/v1/invoices HTTP/1.1
Authorization: Bearer eyJhbGciOiJIUzUxMiJ9...
Paso 2: TenantDatabaseManager extrae tenant
// TenantDatabaseManager.java (SimappeCommons)
public void initializeTenantConnection(String token) throws SimappeException {
// Extrae clientId_environment del claim "sub" del JWT
String tenantId = extractTenantId(token); // Ejemplo: "centrica_prod"
// Consulta SimappeAdmin para obtener config de DB
DatabaseConfigV2Dto config = adminClientService.getConnection(token).block();
if (!config.isValid()) {
throw new SimappeException("Invalid database configuration for tenant: " + tenantId);
}
setupTenantDataSource(tenantId, config);
}
Paso 3: Crear DataSource dinámico
// TenantDatabaseManager.java
private DataSource createDataSource(DatabaseConfigV2Dto config) {
HikariDataSource ds = new HikariDataSource();
// URL COMPLETO con motor específico
ds.setJdbcUrl(config.getUrl()); // jdbc:oracle:thin:@...
ds.setUsername(config.getUsername());
ds.setPassword(config.getPassword());
// Pool config
ds.setMaximumPoolSize(config.getMaxPoolSize());
ds.setMinimumIdle(config.getMinIdle());
ds.setConnectionTimeout(config.getConnectionTimeout());
// Schema si aplica
ds.setSchema(config.getSchema());
return ds;
}
Paso 4: Repository usa DataSource del tenant
@ConnectionContext // Activa routing multi-tenant
@Component
public class InvoiceComponent extends SimappeService<InvoiceDto, Long, Invoice, InvoiceRepository> {
@Transactional(readOnly = true)
public List<InvoiceDto> read(HttpServletRequest request) {
// El repository automáticamente usa el DataSource del tenant actual
return repository.findAll().stream()
.map(this::mapToDto)
.collect(Collectors.toList());
}
}
Maven Archetype que genera la estructura completa de un microservicio Nebula en segundos.
Comando:
mvn archetype:generate \
-DarchetypeGroupId=com.catcsoft.simappe \
-DarchetypeArtifactId=SimappeArchetype \
-DarchetypeVersion=1.0.26-SNAPSHOTS \
-DgroupId=com.centrica.nebula \
-DartifactId=nebula-accounting \
-Dversion=1.0.0-SNAPSHOT
Resultado: Proyecto nebula-accounting con:
SimappeCommons y SimappeModeldeploy-local.sh scriptnebula-accounting/
├── src/main/java/com/centrica/nebula/accounting/
│ ├── config/
│ │ ├── AppConfig.java
│ │ ├── MultiTenantJpaConfig.java
│ │ ├── MongoConfiguration.java
│ │ ├── RedisConfig.java
│ │ └── OpenApiConfig.java
│ ├── v1/
│ │ ├── controller/
│ │ │ └── AsientoContableController.java
│ │ ├── service/
│ │ │ └── AsientoContableService.java
│ │ ├── component/
│ │ │ └── AsientoContableComponent.java
│ │ ├── entity/
│ │ │ └── AsientoContable.java
│ │ ├── repository/
│ │ │ └── AsientoContableRepository.java
│ │ └── kafka/
│ │ └── AuditLogProducer.java
│ └── NebulaAccountingApplication.java
├── src/main/resources/
│ ├── application.yml
│ └── application-dev.yml
├── Dockerfile
├── pom.xml
└── deploy-local.sh
HTTP Request
↓
[Controller] → Valida, extrae params
↓
[Service] → Orquesta, llama a múltiples components
↓
[Component] → Lógica de negocio, transacciones
↓
[Repository] → Acceso a BD (JPA/MongoDB)
↓
Database (Tenant-specific via TenantAwareDataSource)
Ejemplo Completo:
// 1. CONTROLLER
@RestController
@RequestMapping("/api/v1/asiento-contable")
public class AsientoContableController extends SimappeController {
private final AsientoContableService service;
@PostMapping("/create")
public ResponseEntity<AsientoContableDto> create(@RequestBody AsientoContableDto dto) {
return ResponseEntity.ok(service.create(dto));
}
}
// 2. SERVICE
@Service
@RequiredArgsConstructor
public class AsientoContableService {
private final AsientoContableComponent component;
public AsientoContableDto create(AsientoContableDto dto) {
return component.create(dto, null);
}
}
// 3. COMPONENT (Lógica de Negocio)
@Component
@ConnectionContext // ← Activa multi-tenancy
@EntityIdSupport
@RequiredArgsConstructor
public class AsientoContableComponent extends SimappeService<
AsientoContableDto, Long, AsientoContable, AsientoContableRepository
> implements CrudHtppServletRequestComponent<AsientoContableDto, Long> {
private final AsientoContableRepository repository;
private final SimappeModelMapper mapper;
@Transactional
@Override
public AsientoContableDto create(AsientoContableDto dto, HttpServletRequest request) {
// Validaciones de negocio
validateCuentas(dto);
// Mapeo DTO → Entity
AsientoContable entity = mapper.map(dto, AsientoContable.class);
// Persistir (usa DB del tenant actual automáticamente)
AsientoContable saved = repository.save(entity);
// Auditoría (envía evento a Kafka)
auditLogService.sendMessage("ASIENTO_CREATED", saved);
// Retornar DTO
return mapper.map(saved, AsientoContableDto.class);
}
private void validateCuentas(AsientoContableDto dto) {
// Lógica de validación contable
if (dto.getDebito() != dto.getCredito()) {
throw new SimappeException("El asiento no está balanceado");
}
}
}
// 4. REPOSITORY
public interface AsientoContableRepository extends SimappeRepository<AsientoContable, LongId> {
// Métodos custom si se requieren
@Query("SELECT a FROM AsientoContable a WHERE a.periodo = :periodo")
List<AsientoContable> findByPeriodo(@Param("periodo") String periodo);
}
// 5. ENTITY
@Entity
@Table(name = "asiento_contable", schema = "contabilidad")
@Data
@NoArgsConstructor
@AllArgsConstructor
public class AsientoContable extends BusinessBaseEntity<LongId> {
@Column(name = "numero_asiento", nullable = false)
private String numeroAsiento;
@Column(name = "fecha_asiento", nullable = false)
private LocalDate fechaAsiento;
@Column(name = "periodo", length = 7)
private String periodo; // "2025-12"
@Column(name = "debito", precision = 18, scale = 2)
private BigDecimal debito;
@Column(name = "credito", precision = 18, scale = 2)
private BigDecimal credito;
@Column(name = "descripcion", length = 500)
private String descripcion;
// Relaciones, auditoría, etc.
}
Propósito: Gestión centralizada de usuarios, roles, configuraciones, notificaciones.
Base de Datos: PostgreSQL (Obligatorio)
APIs Clave:
/api/v1/user - Gestión de usuarios/api/v2/database-config - Configuración de bases de datos multi-tenant/api/v1/email-notification - Notificaciones por email/api/v1/company - Gestión de empresas/api/v1/role - Control de roles y permisosConfiguración de Nuevo Tenant:
# Crear registro en database_config_v2
curl -X POST http://${SIMAPPE_ADMIN_HOST}/api/v2/database-config/create \
-H "Authorization: Bearer ${ADMIN_TOKEN}" \
-H "Content-Type: application/json" \
-d '{
"name": "Acme Corp Producción",
"code": "acme_prod",
"url": "jdbc:oracle:thin:@${ORACLE_HOST}:1521:${ORACLE_SID}",
"username": "${ACME_DB_USER}",
"password": "${ACME_DB_PASSWORD}",
"database": "NEBULA_ACME_PROD",
"type": "ORACLE",
"clientId": "acme",
"environment": "prod",
"maxPoolSize": 15,
"driverClassName": "oracle.jdbc.OracleDriver",
"hibernateDialect": "org.hibernate.dialect.OracleDialect"
}'
Propósito: Menús personalizados, consecutivos, documentos por cliente.
Base de Datos: Oracle (base de datos del tenant, configurada en database_config_v2)
Uso en Nebula:
Propósito: Emisión y validación de JWT tokens.
Flujo de Login:
Claim sub del JWT:
{
"sub": "centrica_prod", // clientId_environment
"userId": 123,
"roles": ["ADMIN", "CONTADOR"],
"companyId": 456,
"exp": 1735408800
}
Propósito: Routing, rate limiting, seguridad perimetral.
Configuración:
spring:
cloud:
gateway:
routes:
- id: nebula-accounting
uri: lb://NEBULA-ACCOUNTING # Load-balanced vía Eureka
predicates:
- Path=/api/v1/accounting/**
filters:
- StripPrefix=2
- name: CircuitBreaker
args:
name: accountingCircuitBreaker
fallbackUri: forward:/fallback/accounting
Propósito: Registro y descubrimiento de microservicios.
Conexión desde Nebula:
# application.yml
eureka:
client:
serviceUrl:
defaultZone: http://${EUREKA_HOST}:${EUREKA_PORT}/eureka/
registerWithEureka: true
fetchRegistry: true
instance:
preferIpAddress: true
instance-id: ${spring.application.name}:${random.value}
Propósito: Configuraciones externalizadas en repositorio Git.
Estructura Git:
simappe-config-repo/
├── application-common.yml # Config compartida
├── nebula-accounting-dev.yml # Config dev
├── nebula-accounting-prod.yml # Config prod
└── database-connections.yml # Plantillas de conexión
Conexión desde Nebula:
# bootstrap.yml
spring:
application:
name: nebula-accounting
cloud:
config:
uri: http://${CONFIG_SERVER_HOST}:${CONFIG_SERVER_PORT}
profile: ${APP_PROFILE:dev}
label: main
Propósito: Servicios base, multi-tenancy, clientes HTTP, utilidades.
Clases Clave:
// Clase base para servicios CRUD
public abstract class SimappeService<DTO, ID, ENTITY, REPO> {
// CRUD genérico, paginación, búsqueda avanzada
}
// Clase base para controllers REST
public abstract class SimappeController {
// Response wrapping, manejo de errores
}
// Repositorio base con multi-tenancy
public interface SimappeRepository<T, ID> extends JpaRepository<T, ID> {
// Métodos base + extensiones
}
Uso en Nebula:
<!-- pom.xml -->
<dependency>
<groupId>com.catcsoft.simappe</groupId>
<artifactId>SimappeCommons</artifactId>
<version>1.0.26-SNAPSHOTS</version>
</dependency>
Propósito: DTOs de infraestructura (User, Role, DatabaseConfig).
REGLA: DTOs de Nebula van en CentricaModel, NO en SimappeModel.
Propósito: DTOs específicos de negocio Nebula.
Estructura:
CentricaModel/
└── src/main/java/com/centrica/nebula/model/
├── accounting/
│ ├── AsientoContableDto.java
│ ├── CuentaContableDto.java
│ └── PeriodoContableDto.java
├── billing/
│ ├── FacturaDto.java
│ └── ItemFacturaDto.java
├── inventory/
│ └── ProductoDto.java
└── payroll/
└── NominaDto.java
Flujo de Modificación:
CentricaModeldevelopmvn deploy a Nexuspom.xmlPropósito: Interfaces WebClient para comunicación entre microservicios Nebula.
Ejemplo:
// CentricaShared
@HttpExchange("/api/v1/inventory")
public interface InventoryWebClient {
@GetExchange("/product/get")
Mono<ProductoDto> getProduct(@RequestParam("id") Long id);
@PostExchange("/product/stock/reserve")
Mono<Void> reserveStock(@RequestBody ReserveStockRequest request);
}
// Uso en nebula-accounting
@Service
@RequiredArgsConstructor
public class AsientoInventarioService {
private final InventoryWebClient inventoryClient;
public void createAsientoFromStockMovement(Long productId) {
ProductoDto product = inventoryClient.getProduct(productId).block();
// Crear asiento con datos del producto
}
}
Archivos SQL por Motor:
src/main/resources/db/migration/
├── postgresql/
│ ├── V1__create_schema_contabilidad.sql
│ ├── V2__create_table_asiento_contable.sql
│ └── V3__insert_plan_cuentas.sql
├── oracle/
│ ├── V1__create_schema_contabilidad.sql
│ ├── V2__create_table_asiento_contable.sql
│ └── V3__insert_plan_cuentas.sql
└── sqlserver/
├── V1__create_schema_contabilidad.sql
├── V2__create_table_asiento_contable.sql
└── V3__insert_plan_cuentas.sql
Configuración Flyway:
# application.yml
spring:
flyway:
enabled: true
locations: classpath:db/migration/${database.type:oracle}
baseline-on-migrate: true
validate-on-migrate: true
-- V1__create_schema_contabilidad.sql (PostgreSQL)
CREATE SCHEMA IF NOT EXISTS contabilidad;
CREATE TABLE contabilidad.cuenta_contable (
id BIGSERIAL PRIMARY KEY,
codigo VARCHAR(20) NOT NULL UNIQUE,
nombre VARCHAR(200) NOT NULL,
tipo VARCHAR(50) NOT NULL,
naturaleza VARCHAR(10) NOT NULL CHECK (naturaleza IN ('DEBITO', 'CREDITO')),
nivel INT NOT NULL,
cuenta_padre_id BIGINT REFERENCES contabilidad.cuenta_contable(id),
activo BOOLEAN DEFAULT TRUE,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
CREATE INDEX idx_cuenta_codigo ON contabilidad.cuenta_contable(codigo);
CREATE INDEX idx_cuenta_padre ON contabilidad.cuenta_contable(cuenta_padre_id);
-- V1__create_schema_contabilidad.sql (Oracle)
-- Oracle no tiene CREATE SCHEMA como PostgreSQL
-- Crear tablespace si es necesario
CREATE TABLESPACE nebula_data
DATAFILE 'nebula_data.dbf' SIZE 500M
AUTOEXTEND ON NEXT 100M MAXSIZE UNLIMITED;
CREATE TABLE cuenta_contable (
id NUMBER GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
codigo VARCHAR2(20) NOT NULL UNIQUE,
nombre VARCHAR2(200) NOT NULL,
tipo VARCHAR2(50) NOT NULL,
naturaleza VARCHAR2(10) NOT NULL CHECK (naturaleza IN ('DEBITO', 'CREDITO')),
nivel NUMBER NOT NULL,
cuenta_padre_id NUMBER,
activo NUMBER(1) DEFAULT 1,
created_at TIMESTAMP DEFAULT SYSTIMESTAMP,
updated_at TIMESTAMP DEFAULT SYSTIMESTAMP,
CONSTRAINT fk_cuenta_padre FOREIGN KEY (cuenta_padre_id)
REFERENCES cuenta_contable(id)
) TABLESPACE nebula_data;
CREATE INDEX idx_cuenta_codigo ON cuenta_contable(codigo);
CREATE INDEX idx_cuenta_padre ON cuenta_contable(cuenta_padre_id);
-- V1__create_schema_contabilidad.sql (SQL Server)
CREATE SCHEMA contabilidad;
GO
CREATE TABLE contabilidad.cuenta_contable (
id BIGINT IDENTITY(1,1) PRIMARY KEY,
codigo NVARCHAR(20) NOT NULL UNIQUE,
nombre NVARCHAR(200) NOT NULL,
tipo NVARCHAR(50) NOT NULL,
naturaleza NVARCHAR(10) NOT NULL CHECK (naturaleza IN ('DEBITO', 'CREDITO')),
nivel INT NOT NULL,
cuenta_padre_id BIGINT,
activo BIT DEFAULT 1,
created_at DATETIME2 DEFAULT GETDATE(),
updated_at DATETIME2 DEFAULT GETDATE(),
CONSTRAINT FK_cuenta_padre FOREIGN KEY (cuenta_padre_id)
REFERENCES contabilidad.cuenta_contable(id)
);
CREATE INDEX idx_cuenta_codigo ON contabilidad.cuenta_contable(codigo);
CREATE INDEX idx_cuenta_padre ON contabilidad.cuenta_contable(cuenta_padre_id);
Paso 1: Crear Base de Datos Física
-- PostgreSQL
CREATE DATABASE nebula_acme_prod
WITH
OWNER = nebula_admin
ENCODING = 'UTF8'
LC_COLLATE = 'en_US.UTF-8'
LC_CTYPE = 'en_US.UTF-8'
TABLESPACE = pg_default
CONNECTION LIMIT = -1;
-- Oracle
CREATE TABLESPACE NEBULA_ACME_PROD
DATAFILE '/u01/oradata/NEBULA_ACME_PROD.dbf' SIZE 1G;
CREATE USER nebula_acme IDENTIFIED BY ${SECURE_PASSWORD}
DEFAULT TABLESPACE NEBULA_ACME_PROD
QUOTA UNLIMITED ON NEBULA_ACME_PROD;
GRANT CONNECT, RESOURCE TO nebula_acme;
Paso 2: Registrar en database_config_v2 (vía API SimappeAdmin)
POST /api/v2/database-config/create
{
"name": "Acme Corp - Producción",
"code": "acme_prod",
"url": "jdbc:oracle:thin:@${ORACLE_HOST}:1521:${ORACLE_SID}",
"username": "nebula_acme",
"password": "${DB_PASSWORD_ENCRYPTED}",
"database": "NEBULA_ACME_PROD",
"schema": "NEBULA_ACME",
"type": "ORACLE",
"clientId": "acme",
"environment": "prod",
"maxPoolSize": 20,
"minIdle": 5,
"driverClassName": "oracle.jdbc.OracleDriver",
"hibernateDialect": "org.hibernate.dialect.OracleDialect"
}
Paso 3: Ejecutar Migraciones Flyway
# Flyway se ejecuta automáticamente al iniciar el microservicio
# o manualmente:
flyway -url="${JDBC_URL}" \
-user="${DB_USER}" \
-password="${DB_PASSWORD}" \
-locations="filesystem:migrations/oracle" \
migrate
Paso 4: Crear Usuario Admin del Tenant
POST /api/v1/user/create (SimappeAdmin)
{
"username": "admin@acme.com",
"email": "admin@acme.com",
"password": "${INITIAL_PASSWORD}",
"firstName": "Admin",
"lastName": "Acme Corp",
"clientId": "acme",
"roles": ["SUPER_ADMIN"],
"companyId": ${ACME_COMPANY_ID}
}
Paso 5: Validar Conexión
# Request de prueba con JWT del usuario creado
curl -X GET http://${NEBULA_ACCOUNTING_HOST}/api/v1/accounting/cuentas/read \
-H "Authorization: Bearer ${JWT_TOKEN_ACME}"
# Debería retornar lista vacía si no hay dato, o error de conexión si falló
Development (application-dev.yml):
spring:
datasource:
# SimappeAdmin System DB (PostgreSQL)
url: jdbc:postgresql://${DEV_POSTGRES_HOST:localhost}:5432/simappe_admin
username: ${DEV_POSTGRES_USER:postgres}
password: ${DEV_POSTGRES_PASSWORD:postgres}
data:
mongodb:
uri: mongodb://${DEV_MONGO_HOST:localhost}:27017/simappe_audit
redis:
host: ${DEV_REDIS_HOST:localhost}
port: ${DEV_REDIS_PORT:6379}
kafka:
bootstrapAddress: ${DEV_KAFKA_HOST:localhost}:9092
eureka:
client:
serviceUrl:
defaultZone: http://${DEV_EUREKA_HOST:localhost}:8761/eureka/
Production (application-prod.yml):
spring:
datasource:
url: jdbc:postgresql://${PROD_POSTGRES_HOST}:5432/${PROD_POSTGRES_DB}
username: ${PROD_POSTGRES_USER}
password: ${PROD_POSTGRES_PASSWORD}
data:
mongodb:
uri: mongodb://${PROD_MONGO_USER}:${PROD_MONGO_PASSWORD}@${PROD_MONGO_HOST}:27017/${PROD_MONGO_DB}?authSource=admin
redis:
host: ${PROD_REDIS_HOST}
port: ${PROD_REDIS_PORT}
password: ${PROD_REDIS_PASSWORD}
kafka:
bootstrapAddress: ${PROD_KAFKA_BROKER_1},${PROD_KAFKA_BROKER_2},${PROD_KAFKA_BROKER_3}
eureka:
client:
serviceUrl:
defaultZone: http://${PROD_EUREKA_1}:8761/eureka/,http://${PROD_EUREKA_2}:8761/eureka/
version: "3.8"
services:
nebula-accounting:
image: catcsoft/nebula-accounting:latest
environment:
- APP_PROFILE=dev
- DEV_POSTGRES_HOST=postgres
- DEV_POSTGRES_USER=nebula
- DEV_POSTGRES_PASSWORD=${POSTGRES_PASSWORD}
- DEV_MONGO_HOST=mongodb
- DEV_REDIS_HOST=redis
- DEV_KAFKA_HOST=kafka
- DEV_EUREKA_HOST=eureka
ports:
- "8081:8080"
depends_on:
- postgres
- mongodb
- redis
- kafka
- eureka
postgres:
image: postgres:16
environment:
- POSTGRES_DB=simappe_admin
- POSTGRES_USER=nebula
- POSTGRES_PASSWORD=${POSTGRES_PASSWORD}
volumes:
- postgres_data:/var/lib/postgresql/data
ports:
- "5432:5432"
mongodb:
image: mongo:7.0
volumes:
- mongo_data:/data/db
ports:
- "27017:27017"
redis:
image: redis:7-alpine
ports:
- "6379:6379"
kafka:
image: confluentinc/cp-kafka:7.5.0
environment:
- KAFKA_ZOOKEEPER_CONNECT=zookeeper:2181
- KAFKA_ADVERTISED_LISTENERS=PLAINTEXT://kafka:9092
ports:
- "9092:9092"
depends_on:
- zookeeper
zookeeper:
image: confluentinc/cp-zookeeper:7.5.0
environment:
- ZOOKEEPER_CLIENT_PORT=2181
eureka:
image: catcsoft/simappe-eureka-server:latest
ports:
- "8761:8761"
volumes:
postgres_data:
mongo_data:
Nebula ERP se construye en 2 capas:
Multi-Tenancy = DATABASE-BASED:
database_config_v2 en SimappeAdmin define conexionesSimappeArchetype: Genera microservicios completos en segundos
Sin hardcodeo: TODO usa variables de entorno (${DB_HOST}, ${DB_USER}, etc.)
Arquitecto = Diseñador, NO operador: Define planos, Centrica ejecuta
Documento Técnico - Arquitectura Simappe + Nebula
© 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. |