Skip to content
Open
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
Binary file added templates/didacta/assets/logo.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added templates/didacta/assets/screenshot.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
125 changes: 125 additions & 0 deletions templates/didacta/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
import { Output, randomString, Services } from "~templates-utils";
import { Input } from "./meta";

/**
* Didacta Community — plantilla de Easypanel.
*
* Easypanel no despliega docker-compose: la plantilla es esta función, que
* devuelve servicios nativos del panel. Tres servicios:
*
* - `app` → la imagen de Didacta (API :4000 + web :3000 en el mismo
* contenedor). Solo se publica el 3000: Next.js reescribe
* `/api/*`, `/healthz` y `/readyz` al 4000 interno.
* - `postgres` → con imagen pgvector. NO es opcional: la migración baseline
* hace `CREATE EXTENSION "vector"` y falla contra el
* postgres:17 que Easypanel usa por defecto.
* - `redis` → colas y caché.
*/
export function generate(input: Input): Output {
const services: Services = [];

// 64 caracteres sin símbolos: firma las sesiones. Rotarlo echa a todo el
// mundo de su sesión.
const authSecret = randomString(64);
// Token de un solo uso del alta inicial. Viaja en una URL, así que sin
// símbolos a propósito.
const setupToken = randomString(32);
const databasePassword = randomString(32);
const redisPassword = randomString(32);

const databaseHost = `$(PROJECT_NAME)_${input.databaseServiceName}`;
const redisHost = `$(PROJECT_NAME)_${input.redisServiceName}`;
const databaseName = "didacta";

// `DIDACTA_CORE_VERSION` es la fuente de verdad de compatibilidad de módulos
// y tiene que coincidir con el tag de la imagen. Si alguien pega una imagen
// sin tag, se queda vacía en vez de mentir con el nombre del repositorio.
const imageTag = input.appServiceImage.includes(":")
? input.appServiceImage.split(":").pop()
: "";

services.push({
type: "app",
data: {
serviceName: input.appServiceName,
env: [
`NODE_ENV=production`,
`DIDACTA_CORE_VERSION=${imageTag}`,
// Base absoluta de los enlaces que salen por correo (verificación de
// email, invitaciones, recordatorios).
`WEB_PUBLIC_URL=https://$(PRIMARY_DOMAIN)`,
`WEB_PUBLIC_ALLOWED_HOSTS=$(PRIMARY_DOMAIN)`,
// Conexión de BOOTSTRAP (superusuario). Solo migraciones, RLS y
// grants — nunca sirve tráfico. El entrypoint deriva de aquí la
// conexión de runtime hacia el rol `didacta_app` (NOBYPASSRLS), que es
// lo que hace real el aislamiento entre tenants. Por eso no se define
// DATABASE_URL.
`ADMIN_DATABASE_URL=postgresql://postgres:${databasePassword}@${databaseHost}:5432/${databaseName}?schema=public`,
`REDIS_URL=redis://default:${redisPassword}@${redisHost}:6379`,
`AUTH_SECRET=${authSecret}`,
// Fijarlo evita buscar el token en los logs: el alta se hace en
// https://<dominio>/setup?token=<este valor>.
`DIDACTA_SETUP_TOKEN=${setupToken}`,
// Puertos INTERNOS fijos. No tocar.
`API_PORT=4000`,
`WEB_PORT=3000`,
// Almacenamiento local sobre el volumen persistente. Para S3 (AWS,
// Hetzner, MinIO…): STORAGE_DRIVER=s3 y rellenar las S3_*.
`STORAGE_DRIVER=local`,
`STORAGE_ROOT=/app/data/storage`,
// SMTP: sin esto la instalación arranca igual, avisa por la interfaz y
// no puede mandar correos. Rellenar desde el panel tras desplegar.
`SMTP_HOST=`,
`SMTP_PORT=587`,
`SMTP_USER=`,
`SMTP_PASS=`,
`SMTP_SECURE=false`,
`SMTP_FROM=`,
// Licencia Enterprise (opcional). Vacío = edición Community.
`DIDACTA_LICENSE_KEY=`,
// TENANT_SETTINGS_ENC_KEY se deja sin definir a propósito: la app
// genera una clave hex de 32 bytes en el primer arranque y la persiste
// en el volumen `data`.
].join("\n"),
source: {
type: "image",
image: input.appServiceImage,
},
domains: [
{
host: "$(EASYPANEL_DOMAIN)",
port: 3000,
},
],
mounts: [
{
// Uploads de cursos, certificados y evidencias + la clave de cifrado
// autogenerada. Es el volumen que hay que respaldar.
type: "volume",
name: "data",
mountPath: "/app/data",
},
],
},
});

services.push({
type: "postgres",
data: {
serviceName: input.databaseServiceName,
image: "pgvector/pgvector:pg16",
databaseName,
password: databasePassword,
},
});

services.push({
type: "redis",
data: {
serviceName: input.redisServiceName,
password: redisPassword,
},
});

return { services };
}
54 changes: 54 additions & 0 deletions templates/didacta/meta.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
name: Didacta
description: Didacta is a fair-code, self-hosted learning platform (LMS). It ships multi-tenant isolation enforced at the database level (PostgreSQL RLS), a public course catalog with checkout, enrollment and invitation flows, gamification, certificates, reporting and a module marketplace. The API and the web app run in a single container behind one domain.
instructions: |
The first account is created through a one-time setup link. After the deploy
finishes, open:

https://<your-domain>/setup?token=<DIDACTA_SETUP_TOKEN>

`DIDACTA_SETUP_TOKEN` is in the Environment tab of the app service. The link
stops working as soon as the first account is created.

Email is not configured out of the box: fill in the `SMTP_*` variables in the
Environment tab so Didacta can send verification emails, invitations and
reminders. Until then the app runs fine and warns you in the UI.

Back up the `data` volume of the app service — it holds course uploads,
certificates and the auto-generated encryption key.
changeLog:
- date: 2026-8-12
description: first release
links:
- label: Website
url: https://didacta.io
- label: Documentation
url: https://docs.didacta.io
- label: Github
url: https://github.com/va360labs/didacta-io
contributors:
- name: VA360 Labs
url: https://github.com/va360labs
schema:
type: object
required:
- appServiceName
- appServiceImage
- databaseServiceName
- redisServiceName
properties:
appServiceName:
type: string
title: App Service Name
default: didacta
appServiceImage:
type: string
title: App Service Image
default: ghcr.io/va360labs/didacta-community:0.0.1-alpha.107
databaseServiceName:
type: string
title: Database Service Name
default: didacta-db
redisServiceName:
type: string
title: Redis Service Name
default: didacta-redis