DocumentationDocumentación

Install, configure & deploy Meerkat Instala, configura y despliega Meerkat

Everything you need to run Meerkat on your own infrastructure: how the pieces fit together, the configuration reference, single sign-on, notifications, migrations and production deployment on a Linux server plus Cloudflare Pages. Todo lo necesario para correr Meerkat en tu propia infraestructura: cómo encajan las piezas, la referencia de configuración, el inicio de sesión único, las notificaciones, las migraciones y el despliegue en producción en un servidor Linux más Cloudflare Pages.

01OverviewIntroducción

Meerkat is a self-hosted workspace for the whole company: an internal social feed and forums, project boards, a prompt backlog for AI work, training, a people directory with org chart, onboarding, hiring and payroll — all sharing the same users, groups and permissions, behind your single sign-on.

Meerkat es un espacio de trabajo self-hosted para toda la empresa: feed social interno y foros, tableros de proyectos, un backlog de prompts para el trabajo con IA, capacitación, directorio de personas con organigrama, onboarding, reclutamiento y nómina. Todo comparte los mismos usuarios, grupos y permisos, detrás de tu inicio de sesión único.

Feed & ForumsPosts with images, YouTube previews and @mentions; reactions, comments, threaded forums with tags and follow.Posts con imágenes, previews de YouTube y @menciones; reacciones, comentarios y foros con etiquetas y seguimiento.
BoardsKanban/Scrum with Board, List, Calendar, Backlog, Timeline and Insights views; sprints, epics, subtasks, dependencies, custom fields, automations, time tracking.Kanban/Scrum con vistas Board, List, Calendar, Backlog, Timeline e Insights; sprints, épicas, subtareas, dependencias, campos personalizados, automatizaciones y registro de horas.
PromptsA backlog of ready-to-paste AI prompts per project and model, with personal queues, a claimable pool, and “Pop It” (LIFO) / “FIFO It”.Un backlog de prompts de IA listos para pegar por proyecto y modelo, con colas personales, un pool para reclamar y «Pop It» (LIFO) / «FIFO It».
TrainingCourses with markdown, image, video, audio and quiz blocks; required-for-groups; completion reports with scores.Cursos con bloques de markdown, imagen, video, audio y quiz; obligatorios por grupo; reportes de avance con puntajes.
People & Org chartDirectory and bio pages with skills and badges; org chart from reporting lines, with an admin editor and hygiene report.Directorio y perfiles con habilidades e insignias; organigrama según líneas de reporte, con editor para admins y reporte de consistencia.
Hiring & PayrollRequisitions with interview stages, candidates, PDF resumes and a talent pool; compensation history and HR notes for HR only.Búsquedas con etapas de entrevista, candidatos, CV en PDF y talent pool; historial de compensación y notas de RRHH solo para RRHH.

Around them: a welcome tour for new hires, an app launcher for your other SSO apps, a ⌘K command palette with global search, in-app/email/chat notifications, dark & light themes with accent colors, and an installable PWA.

Alrededor: un recorrido de bienvenida para nuevas incorporaciones, un lanzador para tus otras apps con SSO, una paleta de comandos ⌘K con búsqueda global, notificaciones en la app, por email y por chat, temas claro y oscuro con colores de acento, y una PWA instalable.

02ArchitectureArquitectura

Meerkat has two deployables: a Go backend (Fiber, one process) that owns the data and the API, and a React frontend (Vite, PWA) served from Cloudflare Pages. Pages Functions reverse-proxy the API paths to the backend, so the browser only ever talks to one origin.

Meerkat tiene dos piezas desplegables: un backend en Go (Fiber, un solo proceso) que es dueño de los datos y la API, y un frontend en React (Vite, PWA) servido desde Cloudflare Pages. Las Pages Functions hacen de proxy inverso de las rutas de la API hacia el backend, así el navegador solo habla con un origen.

  • PostgreSQL is the only data store. Uploaded files (avatars, attachments, training media, resumes) live on disk under public/uploads, next to the binary.
  • Redis is optional and stores sessions only. Application data is cached in-process.
  • Realtime: GET /api/live is a Server-Sent Events stream that nudges clients to refetch. The hub is in-process, so run a single backend instance; clients fall back to polling every 2 minutes.
  • Background workers drain the notification queue every 20 seconds and run daily maintenance (inbox retention, time-based board automations).
  • PostgreSQL es el único almacén de datos. Los archivos subidos (avatares, adjuntos, material de capacitación, CV) se guardan en disco bajo public/uploads, junto al binario.
  • Redis es opcional y guarda solo sesiones. Los datos de la aplicación se cachean en memoria del proceso.
  • Tiempo real: GET /api/live es un stream de Server-Sent Events que avisa a los clientes para que vuelvan a pedir datos. El hub vive en el proceso, así que corre una sola instancia del backend; los clientes hacen polling cada 2 minutos como respaldo.
  • Workers en segundo plano: vacían la cola de notificaciones cada 20 segundos y corren el mantenimiento diario (retención de la bandeja, automatizaciones de tableros por tiempo).

03RequirementsRequisitos

ComponentComponenteVersion / notesVersión / notas
Go1.24+
Node.js20+ (frontend build, Wrangler)(build del frontend, Wrangler)
PostgreSQLRequired. Any supported release (12+) works — Meerkat uses JSONB, GIN indexes and full-text search.Obligatorio. Cualquier versión con soporte (12+) sirve: Meerkat usa JSONB, índices GIN y búsqueda full-text.
RedisOptional, for sessions. Strongly recommended in production — without it every restart signs everyone out.Opcional, para sesiones. Muy recomendado en producción: sin Redis, cada reinicio cierra la sesión de todos.
OIDC identity providerProveedor de identidad OIDCRequired — it is the only way to sign in. Built for Authentik; any standard OIDC provider exposing sub, email, preferred_username and name via userinfo should work.Obligatorio: es la única forma de iniciar sesión. Pensado para Authentik; cualquier proveedor OIDC estándar que exponga sub, email, preferred_username y name en userinfo debería funcionar.
libwebp + gcc + pkg-configOnly for WebP avatar encoding (CGO build). Without it, build with CGO_ENABLED=0 and avatars are stored as PNG.Solo para codificar avatares en WebP (build con CGO). Sin eso, compila con CGO_ENABLED=0 y los avatares se guardan como PNG.
SMTP, RocketChatOptional, for email and chat notifications.Opcionales, para notificaciones por email y chat.
CloudflareA Pages project for the frontend (or any static host plus a reverse proxy).Un proyecto de Pages para el frontend (o cualquier hosting estático más un proxy inverso).

04Quick start (local)Inicio rápido (local)

Starting from an empty database? Migrations only add to an existing base schema — a few core tables (for example the forum tables and some users columns) predate the migration runner. Restore a schema-only dump from an existing Meerkat deployment before the first boot:

¿Arrancas con una base vacía? Las migraciones solo agregan sobre un esquema base existente: algunas tablas centrales (por ejemplo las de foros y algunas columnas de users) son anteriores al sistema de migraciones. Restaura un dump solo de esquema de una instalación existente de Meerkat antes del primer arranque:

pg_dump --schema-only <source-db> | psql <new-db>

  1. Get the code and create the configObtén el código y crea la configuración
    git clone <your-meerkat-repo> meerkat && cd meerkat
    cp config.sample.json config.json

    Edit config.json for local development — the essentials:

    Edita config.json para desarrollo local; lo esencial:

    {
      "db":    { "host": "localhost", "port": 5432, "user": "meerkat", "name": "meerkat", "sslmode": "disable" },
      "app":   { "port": 3000, "environment": "development" },
      "auth":  { "cookie_secure": false, "cookie_samesite": "Lax" },
      "redis": { "host": "" },
      "oauth": {
        "client_id": "meerkat-dev",
        "redirect_url": "http://localhost:5173/oauth/callback",
        "auth_url": "https://auth.example.com/application/o/authorize/",
        "token_url": "https://auth.example.com/application/o/token/",
        "userinfo_url": "https://auth.example.com/application/o/userinfo/",
        "scopes": "openid email profile"
      }
    }

    Pass secrets through the environment rather than the file: DB_PASSWORD, OAUTH_CLIENT_SECRET.

    Pasa los secretos por variables de entorno en vez del archivo: DB_PASSWORD, OAUTH_CLIENT_SECRET.

  2. Run the backendLevanta el backend
    # migrations apply automatically on start
    export DB_PASSWORD=... OAUTH_CLIENT_SECRET=...
    CGO_ENABLED=0 go run .
    # or, with libwebp installed:  go build -o meerkat . && ./meerkat

    config.json is read from the current working directory, and the port comes only from app.port — there are no command-line flags. If app.port is missing the server picks a random port.

    config.json se lee desde el directorio de trabajo actual y el puerto sale solo de app.port: no hay flags de línea de comandos. Si falta app.port, el servidor elige un puerto al azar.

  3. Run the frontendLevanta el frontend
    cd meerkat-app
    npm install
    npm run dev          # http://localhost:5173 → proxies /api /oauth /avatars /uploads /public to :3000

    Backend on another port? Point the dev proxy at it: MEERKAT_API_PROXY=http://localhost:15601 npm run dev. Also available: dev:alt (5174 → 3100), dev:b (5175 → 3200) and dev:auto.

    ¿El backend corre en otro puerto? Apunta el proxy de desarrollo: MEERKAT_API_PROXY=http://localhost:15601 npm run dev. También existen dev:alt (5174 → 3100), dev:b (5175 → 3200) y dev:auto.

  4. Sign in and promote the first adminInicia sesión y promueve al primer admin

    Open http://localhost:5173 and click Sign in with SSO. Your first login creates your user. There is no bootstrap admin, so grant the first one in SQL — after that, admins manage roles from Admin → Users.

    Abre http://localhost:5173 y haz clic en Sign in with SSO. Tu primer login crea tu usuario. No hay un admin inicial, así que otorga el primero por SQL; después, los admins gestionan los roles desde Admin → Users.

    UPDATE users SET is_admin = true WHERE username = '<your-preferred_username>';

Register http://localhost:5173/oauth/callback as a redirect URI in your identity provider. Since redirect_url is a single value, use a separate OIDC client (or an extra redirect URI) for development and production.

Registra http://localhost:5173/oauth/callback como redirect URI en tu proveedor de identidad. Como redirect_url es un único valor, usa un cliente OIDC separado (o un redirect URI adicional) para desarrollo y producción.

05Configuration — config.jsonConfiguración: config.json

The complete schema with defaults. Templates live in the repo as config.sample.json, config.email.sample.json and config.desktop-oauth.sample.json; config.json itself is git-ignored.

El esquema completo con sus valores por defecto. En el repo hay plantillas: config.sample.json, config.email.sample.json y config.desktop-oauth.sample.json; config.json está ignorado por git.

{
  "db": {
    "host": "localhost", "port": 5432, "user": "meerkat", "name": "meerkat",
    "password": "",            // prefer DB_PASSWORD
    "sslmode": "require"       // empty means "require"; use "disable" for a local Postgres
  },
  "app": {
    "port": 15601,             // no default — always set it
    "environment": "production", // "production" forces secure cookies
    "upload_dir": ""            // default <cwd>/uploads → files under <cwd>/public/uploads
  },
  "oauth": {
    "client_id": "", "client_secret": "",   // prefer OAUTH_CLIENT_SECRET
    "redirect_url": "https://meerkat.example.com/oauth/callback",  // the FRONTEND origin
    "auth_url": "https://auth.example.com/application/o/authorize/",
    "token_url": "https://auth.example.com/application/o/token/",
    "userinfo_url": "https://auth.example.com/application/o/userinfo/",
    "scopes": "openid email profile"
  },
  "redis": { "host": "127.0.0.1", "port": 6379, "password": "", "db": 0 },  // host "" = in-memory sessions
  "auth": {
    "cookie_domain": "",        // empty = host-only (recommended behind the Pages proxy)
    "cookie_secure": true,
    "cookie_samesite": "Lax"    // Lax | Strict | None
  },
  "authentik": {
    "base_url": "",             // optional, derived from oauth.auth_url
    "token": "",                // prefer AUTHENTIK_TOKEN
    "sync_enabled": false,
    "sync_interval": "1h"
  },
  "email": {
    "smtp": {
      "host": "", "port": 587, "username": "", "password": "",   // prefer SMTP_PASSWORD
      "from_address": "", "from_name": "Meerkat",
      "use_tls": false, "use_starttls": true, "insecure_skip_verify": false
    }
  }
}
  • Sessions last 15 days in a cookie named hr.sid (HttpOnly). auth.session_cookie_name and auth.session_ttl_minutes exist in the schema but are not used.
  • Back up <working-dir>/public/uploads together with the database — it holds avatars, attachments, training media and resumes.
  • Las sesiones duran 15 días en una cookie llamada hr.sid (HttpOnly). auth.session_cookie_name y auth.session_ttl_minutes existen en el esquema pero no se usan.
  • Respalda <directorio-de-trabajo>/public/uploads junto con la base de datos: ahí están los avatares, adjuntos, material de capacitación y CV.

06Environment variablesVariables de entorno

A non-empty environment variable overrides the value from config.json. Use them for secrets (for example in a systemd EnvironmentFile).

Una variable de entorno no vacía reemplaza el valor de config.json. Úsalas para los secretos (por ejemplo, en un EnvironmentFile de systemd).

VariableVariableOverridesReemplaza
DB_HOST DB_PORT DB_USER DB_PASSWORD DB_NAME DB_SSLMODEdb.*
REDIS_HOST REDIS_PORT REDIS_PASSWORDredis.*
OAUTH_CLIENT_SECREToauth.client_secret
SMTP_PASSWORDemail.smtp.password
AUTHENTIK_BASE_URL AUTHENTIK_TOKENauthentik.base_url, authentik.token
APP_BASE_URLPublic frontend origin used in links inside emails and chat messages (fallback for the app_base_url system setting). Set it when self-hosting.Origen público del frontend que se usa en los enlaces de emails y mensajes de chat (respaldo del ajuste de sistema app_base_url). Configúralo si haces self-hosting.
ROCKETCHAT_WEBHOOK_URLFallback for the rocketchat_webhook_url system setting.Respaldo del ajuste de sistema rocketchat_webhook_url.
MEERKAT_API_PROXY, PORTFrontend dev server only: proxy target and port.Solo para el servidor de desarrollo del frontend: destino del proxy y puerto.

System settingsAjustes de sistema

A few runtime settings live in the system_config table and are edited by admins through GET/PUT /api/admin/config. A non-empty database value wins over the environment variable of the same name in uppercase.

Algunos ajustes viven en la tabla system_config y los editan los admins con GET/PUT /api/admin/config. Un valor no vacío en la base tiene prioridad sobre la variable de entorno del mismo nombre en mayúsculas.

curl -X PUT https://meerkat.example.com/api/admin/config \
  -H 'Content-Type: application/json' --cookie 'hr.sid=…' \
  -d '{"key":"app_base_url","value":"https://meerkat.example.com"}'

07Authentication & SSOAutenticación y SSO

People sign in exclusively through your OIDC provider (authorization-code flow). On first login Meerkat creates the user; afterwards the identity provider stays the source of truth for username, display name and email.

La gente inicia sesión exclusivamente a través de tu proveedor OIDC (flujo authorization code). En el primer login Meerkat crea al usuario; después, el proveedor de identidad sigue siendo la fuente de verdad para el nombre de usuario, el nombre visible y el email.

Set up AuthentikConfigura Authentik

  1. Create an OAuth2/OpenID providerCrea un provider OAuth2/OpenID

    Client type Confidential, redirect URI https://meerkat.example.com/oauth/callback (plus the localhost one for development), scopes openid email profile.

    Tipo de cliente Confidential, redirect URI https://meerkat.example.com/oauth/callback (más el de localhost para desarrollo), scopes openid email profile.

  2. Create an application bound to that providerCrea una aplicación vinculada a ese provider

    Copy the client ID into oauth.client_id and the secret into OAUTH_CLIENT_SECRET.

    Copia el client ID en oauth.client_id y el secreto en OAUTH_CLIENT_SECRET.

  3. Fill in the endpointsCompleta los endpoints

    Authentik exposes them at /application/o/authorize/, /application/o/token/ and /application/o/userinfo/.

    Authentik los expone en /application/o/authorize/, /application/o/token/ y /application/o/userinfo/.

The callback must be on the frontend origin: Cloudflare Pages proxies /oauth/* to the backend, and the session cookie is host-only on that origin.

El callback debe estar en el origen del frontend: Cloudflare Pages hace de proxy de /oauth/* hacia el backend y la cookie de sesión queda ligada a ese origen.

Directory sync (optional)Sincronización del directorio (opcional)

OIDC can only describe the person signing in, so Meerkat can also pull the whole directory from Authentik's REST API. That way people appear in the directory and org chart before their first login.

OIDC solo describe a la persona que inicia sesión, así que Meerkat también puede traer el directorio completo desde la API REST de Authentik. Así las personas aparecen en el directorio y el organigrama antes de su primer login.

  • Create an Authentik service account allowed to read users and put its token in AUTHENTIK_TOKEN.
  • Set authentik.sync_enabled: true and a sync_interval (default 1h), or run it on demand from Admin → System, including a dry-run preview.
  • Sync creates missing people, fills empty job titles, updates emails and names, and sets a manager when the person has none (from attributes.manager). It never deletes or deactivates anyone.
  • Crea en Authentik una service account con permiso para leer usuarios y pon su token en AUTHENTIK_TOKEN.
  • Configura authentik.sync_enabled: true y un sync_interval (por defecto 1h), o córrela a demanda desde Admin → System, con vista previa incluida.
  • La sincronización crea a las personas que faltan, completa puestos vacíos, actualiza emails y nombres, y asigna un responsable cuando la persona no tiene (desde attributes.manager). Nunca borra ni desactiva a nadie.

08Roles & permissionsRoles y permisos

FlagFlagGrantsOtorga
is_adminAdmin panel (users, groups, badges, skills, apps, API keys, system). Also satisfies the HR and hiring checks.Panel de admin (usuarios, grupos, insignias, habilidades, apps, API keys, sistema). También cumple los chequeos de RRHH y reclutamiento.
can_hireHiring and the Talent pool.Reclutamiento y Talent pool.
is_hrPayroll and HR records.Nómina y registros de RRHH.
Board rolesRoles de tableroowner, admin, viewer or member; boards are open or private.owner, admin, viewer o miembro; los tableros son abiertos o privados.

Every check is enforced server-side; the sidebar simply hides modules a person can't open. Admins toggle flags in Admin → Users.

Todos los chequeos se aplican en el servidor; la barra lateral simplemente oculta los módulos que una persona no puede abrir. Los admins cambian los flags en Admin → Users.

09NotificationsNotificaciones

Assignments, comments, mentions, forum replies and reactions create notifications. Each person chooses channels and event types in Profile → Notifications.

Las asignaciones, comentarios, menciones, respuestas en foros y reacciones generan notificaciones. Cada persona elige canales y tipos de evento en Profile → Notifications.

ChannelCanalSetupConfiguración
In-app inboxBandeja en la appAlways on; pushed live over SSE. Read items are kept 90 days, everything else 180.Siempre activa; llega en vivo por SSE. Las leídas se guardan 90 días; el resto, 180.
EmailSet email.smtp in config.json (and SMTP_PASSWORD). Enabled by default per person. Test with POST /api/admin/test-email.Configura email.smtp en config.json (y SMTP_PASSWORD). Activado por defecto para cada persona. Pruébalo con POST /api/admin/test-email.
RocketChatSet the rocketchat_webhook_url and rocketchat_enabled=true system settings; each person adds their chat handle during the welcome tour or in their profile.Configura los ajustes de sistema rocketchat_webhook_url y rocketchat_enabled=true; cada persona agrega su usuario de chat en el recorrido de bienvenida o en su perfil.

External channels are queued and delivered by a background worker every 20 seconds, with up to three retries. Set APP_BASE_URL so links point to your domain.

Los canales externos se encolan y un worker los envía cada 20 segundos, con hasta tres reintentos. Configura APP_BASE_URL para que los enlaces apunten a tu dominio.

10Database & migrationsBase de datos y migraciones

SQL migrations are embedded in the binary and applied automatically at startup, in filename order, each in its own transaction. Applied versions are tracked in schema_migrations. If one file fails it is logged and retried on the next start, while the rest still apply.

Las migraciones SQL están embebidas en el binario y se aplican solas al arrancar, en orden de nombre de archivo y cada una en su propia transacción. Las versiones aplicadas se registran en schema_migrations. Si un archivo falla, se registra en el log y se reintenta en el próximo arranque, mientras el resto se sigue aplicando.

# run the same runner by hand (reads SQL from disk)
go run ./cmd/migrate -config ./config.json -dir ./migrations

# tests
go test ./...

Writing a migration? Add migrations/migration-YYYYMMDD[x]_name.sql. Don't add your own transaction control beyond a single outer BEGIN; … COMMIT;, and keep statements idempotent (IF NOT EXISTS). Files using CONCURRENTLY run statement by statement outside a transaction.

¿Escribes una migración? Agrega migrations/migration-YYYYMMDD[x]_nombre.sql. No agregues control de transacciones propio más allá de un único BEGIN; … COMMIT; externo, y mantén las sentencias idempotentes (IF NOT EXISTS). Los archivos con CONCURRENTLY corren sentencia por sentencia fuera de una transacción.

11DeploymentDespliegue

Backend on a Linux serverBackend en un servidor Linux

Build natively on the server (CGO for WebP avatars needs gcc, pkg-config and libwebp-dev), then run the binary under systemd from the directory that holds config.json and public/uploads.

Compila directamente en el servidor (CGO para avatares WebP necesita gcc, pkg-config y libwebp-dev) y corre el binario con systemd desde el directorio que contiene config.json y public/uploads.

sudo apt install -y golang gcc pkg-config libwebp-dev
CGO_ENABLED=1 go build -o /opt/meerkat/meerkat-server .

Example unit — adjust paths and user to your server:

Unidad de ejemplo; ajusta rutas y usuario a tu servidor:

# /etc/systemd/system/meerkat.service
[Unit]
Description=Meerkat backend
After=network.target postgresql.service

[Service]
WorkingDirectory=/opt/meerkat
ExecStart=/opt/meerkat/meerkat-server
EnvironmentFile=/etc/meerkat.env      # DB_PASSWORD, OAUTH_CLIENT_SECRET, AUTHENTIK_TOKEN, SMTP_PASSWORD, APP_BASE_URL
Restart=on-failure
User=meerkat

[Install]
WantedBy=multi-user.target
sudo systemctl daemon-reload && sudo systemctl enable --now meerkat
journalctl -u meerkat -f

Expose the backend over HTTPS on its own origin (for example https://api.example.com) behind your usual reverse proxy. Run a single instance — realtime updates are in-process.

Expón el backend por HTTPS en su propio origen (por ejemplo https://api.example.com) detrás de tu proxy inverso habitual. Corre una sola instancia: las actualizaciones en tiempo real viven en el proceso.

The bundled deploy.shEl deploy.sh incluido

The repository ships a deploy script that checks git is in sync, compiles locally as a sanity check, rsyncs the source to the server, builds there, swaps the binary with a backup, restarts the service, health-checks Postgres and the migration log, and rolls back automatically on failure.

El repositorio incluye un script de despliegue que verifica que git esté sincronizado, compila localmente como control, sincroniza el código al servidor con rsync, compila ahí, reemplaza el binario guardando un respaldo, reinicia el servicio, chequea la salud de Postgres y el log de migraciones, y hace rollback automático si algo falla.

BACKEND_REMOTE_HOST=deploy@server.example.com \
BACKEND_REMOTE_DIR=/opt/meerkat \
SERVICE_NAME=meerkat HEALTH_PORT=15601 \
./deploy.sh all            # backend | frontend | migrations | all   ·   --dry-run  --no-rollback

Frontend on Cloudflare PagesFrontend en Cloudflare Pages

  1. Point the proxy at your backendApunta el proxy a tu backend

    The Pages Functions in meerkat-app/functions/{api,oauth,avatars,uploads,img}/[[path]].js forward requests to a backend origin written in each file. Replace it in all five:

    Las Pages Functions en meerkat-app/functions/{api,oauth,avatars,uploads,img}/[[path]].js reenvían las peticiones a un origen de backend escrito en cada archivo. Reemplázalo en los cinco:

    const backendUrl = `https://api.example.com${url.pathname}${url.search}`;
  2. Update the other hard-coded originsActualiza los otros orígenes fijos

    The CORS allow-list in main.go, the Workbox API URL patterns in meerkat-app/vite.config.js, and the project name in meerkat-app/wrangler.toml / deploy.sh.

    La lista de CORS permitidos en main.go, los patrones de URL de la API en Workbox en meerkat-app/vite.config.js, y el nombre del proyecto en meerkat-app/wrangler.toml / deploy.sh.

  3. Build and deployCompila y despliega
    cd meerkat-app
    npx wrangler login                       # first time only
    npm run deploy                           # = vite build + wrangler pages deploy dist --project-name=<project>

    Add your custom domain in the Pages dashboard. _redirects provides the SPA fallback and _headers sets long-lived caching for hashed assets.

    Agrega tu dominio en el panel de Pages. _redirects resuelve el fallback de la SPA y _headers configura caché de larga duración para los assets con hash.

Uploads pass through a Pages Function, so Cloudflare's request-body limit for your plan applies (100 MB on Free/Pro) even though the backend accepts training videos up to 500 MB.

Las subidas pasan por una Pages Function, así que aplica el límite de tamaño de petición de tu plan de Cloudflare (100 MB en Free/Pro), aunque el backend acepta videos de capacitación de hasta 500 MB.

12Health & logsSalud y logs

EndpointAuthAuthReturnsDevuelve
GET /api/health/postgres—{ok, latency_ms}
GET /api/health/redis—In-process cache status (despite the name)Estado de la caché en proceso (pese al nombre)
GET /version—Backend build versionVersión del build del backend
GET /api/admin/healthadminPostgres stats, session store, cache, API-key activityEstadísticas de Postgres, almacén de sesiones, caché, actividad de API keys

Access and application logs go to /var/log/meerkat/meerkat.log (rotated), falling back to ./logs/meerkat.log. Early boot output — config, database connection, migrations — goes to the journal. Every response carries an X-Log-Sink header with the active destination.

Los logs de acceso y de aplicación van a /var/log/meerkat/meerkat.log (con rotación) y, si no se puede, a ./logs/meerkat.log. La salida del arranque (configuración, conexión a la base, migraciones) va al journal. Cada respuesta incluye un header X-Log-Sink con el destino activo.

13REST API

Scripts, bots and internal tools use /api/v1 with API keys created in Admin → API Keys. Keys are shown once, stored hashed, can expire, and revoke instantly. Writes are authored by a system user.

Scripts, bots y herramientas internas usan /api/v1 con API keys creadas en Admin → API Keys. Las keys se muestran una sola vez, se guardan hasheadas, pueden vencer y se revocan al instante. Las escrituras quedan firmadas por un usuario de sistema.

curl https://meerkat.example.com/api/v1/feed \
  -H 'Authorization: Bearer bna_XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX'

curl -X POST https://meerkat.example.com/api/v1/tasks \
  -H 'Authorization: Bearer bna_…' -H 'Content-Type: application/json' \
  -d '{"board_id": 12, "title": "Rotate staging certificates"}'
ScopeRoutesRutas
feed:read / feed:writeGET /feed, GET /posts/:id, POST /posts, POST /comments
projects:read / projects:writeGET /projects[/:id], GET /tasks/:id, POST /tasks, PATCH /tasks/:id, POST /tasks/:id/comments
directory:readGET /directory/people[/:id]
search:readGET /search?q=
reports:read / hr:readGET /reports/projects, /reports/activity, /reports/hiring

Wildcards * and resource:* are accepted. The key can also be sent as X-API-Key. Errors: 401 for a missing, invalid or expired key; 403 for insufficient scope. Limits: 1000 requests/min per key.

Se aceptan los comodines * y recurso:*. La key también se puede enviar como X-API-Key. Errores: 401 si la key falta, es inválida o venció; 403 si no alcanza el scope. Límite: 1000 peticiones/min por key.

14Desktop autodiscoveryAutodiscovery de escritorio

Desktop and mobile clients can discover the OIDC configuration from the backend origin, without manual setup:

Los clientes de escritorio y móviles pueden descubrir la configuración OIDC desde el origen del backend, sin configuración manual:

curl https://api.example.com/.well-known/meerkat-desktop
{
  "version": 1,
  "meerkat_base_url": "https://api.example.com",
  "authentik": {
    "issuer": "https://auth.example.com/application/o",
    "authorize_url": "…/authorize/", "token_url": "…/token/", "userinfo_url": "…/userinfo/",
    "client_id": "…", "scopes": "openid email profile",
    "redirect_uri": "http://localhost:34567/callback",
    "pkce_required": true
  }
}

This endpoint is served by the backend directly (it is not proxied by Pages) and cached for five minutes.

Este endpoint lo sirve el backend directamente (Pages no lo proxea) y se cachea cinco minutos.

15TroubleshootingSolución de problemas

“invalid oauth state” after signing in«invalid oauth state» después de iniciar sesión

The session cookie didn't survive the round-trip to the identity provider. Leave cookie_domain empty behind the Pages proxy, use cookie_secure: false on plain-HTTP development, and make sure the backend didn't restart mid-login while using in-memory sessions.

La cookie de sesión no sobrevivió al ida y vuelta con el proveedor de identidad. Deja cookie_domain vacío detrás del proxy de Pages, usa cookie_secure: false en desarrollo sin HTTPS y verifica que el backend no se haya reiniciado a mitad del login usando sesiones en memoria.

“Sign in with SSO” shows the app instead of the login page«Sign in with SSO» muestra la app en vez del login

An old service worker is serving the cached app for /oauth/login. Current builds exclude backend paths; hard-reload or unregister the service worker once.

Un service worker viejo está sirviendo la app en caché para /oauth/login. Los builds actuales excluyen las rutas del backend; haz una recarga forzada o desregistra el service worker una vez.

The identity provider rejects the redirect URIEl proveedor de identidad rechaza el redirect URI

oauth.redirect_url must match a registered redirect URI exactly and live on the frontend origin, e.g. https://meerkat.example.com/oauth/callback.

oauth.redirect_url debe coincidir exactamente con un redirect URI registrado y estar en el origen del frontend, por ejemplo https://meerkat.example.com/oauth/callback.

Everyone is signed out after every deployTodos pierden la sesión después de cada despliegue

Sessions are in memory. Configure Redis and check the boot log for using in-memory session store.

Las sesiones están en memoria. Configura Redis y busca using in-memory session store en el log de arranque.

The admin menu is missingNo aparece el menú de admin

There is no bootstrap admin. Set is_admin in SQL for the first person (see Quick start, step 4).

No hay un admin inicial. Asigna is_admin por SQL a la primera persona (ver Inicio rápido, paso 4).

go build fails on go-webpgo build falla en go-webp

libwebp headers are missing. Install libwebp-dev and pkg-config, or build with CGO_ENABLED=0 (avatars are then stored as PNG).

Faltan los headers de libwebp. Instala libwebp-dev y pkg-config, o compila con CGO_ENABLED=0 (los avatares se guardan entonces como PNG).

Local Postgres fails with an SSL errorEl Postgres local falla con un error de SSL

An empty sslmode means require. Set "sslmode": "disable" for a local database without TLS.

Un sslmode vacío equivale a require. Configura "sslmode": "disable" para una base local sin TLS.

Emails or chat messages are not deliveredNo llegan los emails o mensajes de chat

Check email.smtp.host/port in config.json, that people have an email and the channel enabled, and notification_queue.last_error. For RocketChat, set the rocketchat_enabled system setting to true — the seeded database value takes precedence over the environment variable.

Revisa email.smtp.host/port en config.json, que las personas tengan email y el canal activado, y notification_queue.last_error. Para RocketChat, pon el ajuste de sistema rocketchat_enabled en true: el valor precargado en la base tiene prioridad sobre la variable de entorno.

Board updates only reach some peopleLas actualizaciones de tableros solo le llegan a algunos

Realtime assumes one backend process. Run a single instance; clients still refresh every two minutes as a fallback.

El tiempo real asume un solo proceso de backend. Corre una sola instancia; los clientes igual se refrescan cada dos minutos como respaldo.

Links in emails point to the wrong domainLos enlaces de los emails apuntan al dominio equivocado

Set APP_BASE_URL (or the app_base_url system setting) to your frontend origin.

Configura APP_BASE_URL (o el ajuste de sistema app_base_url) con el origen de tu frontend.

Ready to see it in action?¿Listo para verlo en acción?

Click around the interactive preview on the home page.Explora la vista previa interactiva de la página principal.

Open the live previewAbrir la vista previa