Skolerom | Automata
secciones (10)
$ cd .. // volver a proyectos
automata@latam: ~/proyectos/skolerom
CASO DE USO · slug: skolerom ·2025 · 4 min read · 924 words

> Skolerom

// LMS K-12 usado por escuelas noruegas: backend dual-store (PostgreSQL + MongoDB), frontend React, deploy Docker Compose multi-environment via GitLab CI.

Laravel 11React 18PostgreSQLMongoDBElasticsearchDocker ComposeGitLab CI
Skolerom
▸ rol
Senior Backend · DevOps
▸ status
online
// section 01 · descubrimiento

$ cat ./descubrimiento.md

▸ descripcion

Skolerom (app.skolerom.no) es el LMS que usan las escuelas noruegas K-12 para clases, tareas, contenido y evaluaciones. La API en Laravel maneja usuarios, cursos, notas y mapeos de curriculum; MongoDB guarda el contenido flexible y muy editado (drafts de tareas, teaching paths) mientras PostgreSQL (con ltree) almacena la data relacional y jerarquica estructurada. El frontend React renderea las vistas de docente y alumno. La busqueda corre en Elasticsearch. El deploy es un docker-compose por entorno + GitLab CI por repo (back / front / search / devops).

▸ problema

Una plataforma noruega K-12 multi-tenant tiene que modelar el curriculum UDIR nacional, aislar data por colegio, enviar features rapido a traves de tres repos independientes y mantener produccion estable a traves de dev/stage/prod. Hacer esto con un solo store SQL fuerza cambios de schema por cada ajuste de contenido; con un solo Mongo se pierden las garantias transaccionales.

▸ audiencia

Escuelas noruegas K-12, docentes y estudiantes; directores TI que gestionan deploys por colegio.

// section 03 · arquitectura

$ cat ./arquitectura.md

// section 03b · secuencias

$ cat ./secuencias.md

// flujos en runtime — quien habla con quien y en que orden

// Una accion del user que escribe en PostgreSQL (estructurado) y MongoDB (flexible) sin dejar estado parcial.

Escritura cross-store con rollback · flow-01
loading…
// section 04 · infraestructura

$ cat ./infraestructura.md

▸ servicios
provider: Laravel 11 + PostgreSQL + MongoDB + Redis + Elasticsearch en AWS
  • php-api (HTTP Laravel)
  • php-queue-worker (cola Laravel)
  • nginx (reverse proxy)
  • postgres 16 (relacional + ltree)
  • mongo 4.4 (contenido flexible)
  • redis 7 (cache + cola)
  • elasticsearch (busqueda de curriculum)
  • S3 (storage de archivos)
// section 05 · implementacion

$ cat ./implementacion.md

▸ frontend
  • · React 18
  • · TypeScript
  • · Redux
  • · Webpack
  • · Tailwind
▸ backend
  • · Laravel 11
  • · PHP 8.2
  • · Passport (OAuth2)
  • · spatie/laravel-permission
  • · mongodb/laravel-mongodb 4.9
  • · Doctrine DBAL
  • · spatie/laravel-fractal
▸ datos
  • · PostgreSQL 16 con ltree (curriculum jerarquico)
  • · MongoDB 4.4 (drafts + contenido flexible)
  • · Redis 7 (cache + colas)
  • · Elasticsearch (busqueda)
  • · AWS S3
▸ devops
  • · Docker Compose con overrides por entorno (dev / staging2 / production / new-production)
  • · GitLab CI por repo (back, front, search, devops)
  • · Sentry + Logtail
  • · Bitbucket Pipelines en algunos repos
// section 06 · desafios tecnicos

$ cat ./challenges/*.md

// 3 problemas tecnicos resueltos

01 / 03
challenge-01.md · multi-store · consistencia · dominio
▸ problema

Mantener PostgreSQL y MongoDB consistentes cuando una sola accion del user (publicar un teaching path) escribe en los dos.

restriccion: No hay transaccion cross-store. Una escritura a MongoDB que falla despues del commit a PostgreSQL deja al sistema reportando un path publicado que el frontend no puede leer completo. Hacerlo en el orden inverso tiene el mismo failure mode simetrico.

▸ enfoque

Toda escritura cross-store pasa por una capa de servicio (ej. StoreTeachingPathValidationService) que hace PostgreSQL primero (transaccional), despues MongoDB. Si MongoDB falla, el servicio rollbackea la fila de PostgreSQL por el mismo use case y emite un evento para la cola de reintentos. El cache (Redis) se invalida ultimo, asi un estado parcial nunca es legible.

app/Service/StoreTeachingPathValidationService.php php
// Patron (simplificado):
DB::transaction(function () use ($payload) {
    $row = TeachingPath::create($payload->structured());   // PostgreSQL
    try {
        $this->mongo->upsertDraft($row->id, $payload->flexible());
    } catch (\Throwable $e) {
        // throw rollbackea la transaccion PG
        throw new MongoSyncFailed($row->id, $e);
    }
    Cache::tags('teaching_paths')->forget($row->id);
});
challenge-02.md · devops · docker · gitlab
▸ problema

Deployar cuatro repos versionados independientemente (back, front, search, devops) a un stack coherente dev / staging / production.

restriccion: Cada repo tiene su pipeline GitLab. Produccion corre en AWS con las mismas imagenes que dev local. La config especifica de entorno no puede filtrarse en la imagen; los rollbacks tienen que poder hacerse sin rebuildear.

▸ enfoque

El repo devops es dueño del docker-compose.yml y un archivo de override por entorno (dev / staging2 / production / new-production). El pipeline GitLab de cada repo de app buildea una imagen, la pushea al registry, y la tagea por commit SHA. El pipeline del repo devops elige el SHA via env var y re-deploya. El frontend buildea a un artefacto estatico que se pushea a S3 y se sirve via nginx.

devops/base/skolerom-devops/docker-compose.yml yaml
services:
  php-api:
    build:
      context: ./../temabok-back
      dockerfile: devops/php-api/Dockerfile
    networks: [backend]

  nginx:
    build:
      context: ./nginx
      dockerfile: Dockerfile
    depends_on: [php-api]

# Override por entorno:
#   docker compose -f docker-compose.yml \
#                  -f environments/dev/docker-compose.dev.yml up --build
challenge-03.md · migrations · schema · ops
▸ problema

Manejar 265 migraciones PostgreSQL + cambios de schema MongoDB sin conflictos cuando dos devs cortan migraciones en paralelo.

restriccion: Las migraciones deben ser idempotentes y correr una vez por entorno. La data UDIR (curriculum nacional) tiene que sembrarse en un orden conocido para que las FKs caigan bien. Dev/stage/prod trackean estado de migracion independientemente.

▸ enfoque

Sistema de migraciones de Laravel para PostgreSQL (ordenado por timestamp, natural). Seeders custom para curriculum UDIR que chequean existencia antes de insertar. Los volumes de Docker Compose persisten estado de DB por entorno, asi los re-runs en dev no destruyen data. CI hace un dry-run migrate contra una copia de staging antes de promover.

database/migrations/ bash
# Levantar el stack dev desde checkout limpio:
docker compose -f docker-compose.yml \
               -f environments/dev/docker-compose.dev.yml \
               up --build

# Las migraciones corren dentro del container php-api al bootear:
php artisan migrate --force
php artisan db:seed --class=UdirCurriculumSeeder
// section 07 · testing & ci

$ cat ./testing.md

▸ estrategia

Suite Pest 3 (36 archivos de test) sobre SQLite in-memory para unit rapido y una capa de integracion aparte que pega a Postgres + MongoDB reales dentro de Docker. Los flujos SSO (Dataporten / Feide / JWT) se testean con JWKS stubeados. Los chequeos de permisos via spatie/laravel-permission tienen unit tests por rol.

▸ herramientas
Pest 3.8PHPUnit (via Pest)Mockery 1.6GitLab CIBitbucket Pipelines
// section 09 · resultados

$ cat ./resultados.md

01 /
2753
commits en el repo de backend
02 /
55
modelos Eloquent
03 /
265
migrations PostgreSQL
04 /
36
archivos Pest
05 /
4
entornos de deploy (dev, staging2, production, new-production)
▸ outcomes

Sistema en produccion sirviendo escuelas noruegas en app.skolerom.no. El backend maneja 265+ versiones de schema entre PostgreSQL + MongoDB. CI deploya en merge a master. El Docker Compose multi-environment permite levantar entornos nuevos con un solo archivo de override.

// section 10 · lecciones

$ cat ./lessons.md

// si lo hiciera de nuevo

  • 01 /

    La capa de servicio es la unica frontera segura entre dos stores

    Dejar que los controllers escribieran directo a Postgres y Mongo llevaba a estados parciales que el frontend renderaba contento. Canalizar toda escritura cross-store por un solo servicio hizo el failure mode explicito y el path de rollback obvio.

  • 02 /

    Overrides de Compose por entorno le ganan a un solo config grande

    Al principio el equipo manejaba staging y production en el mismo compose con if-branches. Se rompio en la primera caida real. Partirlo en una base + un override por entorno hizo el diff entre entornos grep-able y los rollbacks un cambio de un archivo.

// relacionados

$ grep -l "tech" ./projects/*

// proyectos que comparten parte del stack

// siguiente paso

$ automata deploy --tu-operacion

// Conversemos sobre como adaptamos esto a tu caso.

./contactar.sh