Data API de Ecosoftware
Una sola puerta autenticada por HTTPS para las tres bases de datos compartidas de la compañía: almacenamiento de objetos (S3), PostgreSQL y MongoDB. Tus aplicaciones hacen CRUD con un token y solo pueden tocar exactamente los recursos que su credencial tiene concedidos.
Qué resuelve
Sin el gateway, cada aplicación necesitaría las credenciales nativas de cada motor (llaves S3, DSN de Postgres, URI de Mongo) y acceso de red a sus puertos. Eso significa repartir credenciales privilegiadas y exponer puertos de base de datos. El Data API invierte el modelo:
- Un solo protocolo — HTTPS + JSON. Sirve desde cualquier lenguaje, cualquier red, sin drivers ni VPN.
- Una sola credencial por aplicación — un
client_id/client_secretemitido en Authentik, revocable y rotable sin tocar las bases de datos. - Permisos por recurso — la credencial se limita a buckets, tablas y
colecciones concretos. Lo que no está concedido responde
403. - Los puertos crudos nunca se exponen a internet — Postgres, Mongo y S3 solo son alcanzables por la VPN; el gateway es la única superficie pública.
¿Cuándo NO usar el API? Para ingesta masiva o consultas SQL complejas, conéctate
de forma nativa por la VPN con el SDK (EcoDB) o con tu driver de siempre: el
API está pensado para CRUD y para aplicaciones que viven fuera de la red interna.
Ver SDK de Python.
Arquitectura
Tu aplicación nunca habla con las bases de datos: habla con el gateway, y el gateway habla con ellas por la red interna de Docker usando credenciales privilegiadas que nunca salen del servidor.
Tu aplicación Ecosoftware (servidor)
┌──────────────┐ ┌──────────────────────────────────────────┐
│ │ │ │
│ client_id │ 1. token │ Authentik (IAM) │
│ client_secret├────────────────▶│ emite un JWT firmado con tus grupos │
│ │◀────── JWT ──────┤ y permisos (eco_grants) │
│ │ │ │
│ │ 2. petición │ Caddy ──▶ ecodb-gateway :8095 │
│ Authorization├────────────────▶│ │ │
│ Bearer JWT │ │ │ valida firma + permisos │
│ │ │ ▼ │
│ │ │ ┌──────────┬──────────┬─────────────┐ │
│ │◀───── JSON ──────┤ │ SeaweedFS│ Postgres │ MongoDB │ │
└──────────────┘ │ │ (S3) │ pgvector │ │ │
│ └──────────┴──────────┴─────────────┘ │
│ puertos internos, nunca públicos │
└──────────────────────────────────────────┘
Qué valida el gateway en cada petición
- Autenticación — la cabecera
Authorization: Bearer …. Si es un JWT de Authentik verifica la firma RS256 contra el JWKS del emisor y su expiración; si es un token estáticoecok_…, compara su hash SHA-256. - Autorización — traduce la petición a una tripleta
(servicio, recurso, permiso) — por ejemplo (s3, eco-cas, write) — y la
contrasta con los
grantsde la credencial. - Ejecución — solo si ambas pasan, ejecuta la operación contra el motor correspondiente y devuelve JSON.
URLs base
| Entorno | Base del API | Emisor de tokens (Authentik) |
|---|---|---|
| Internet / VPN producción |
https://ecosoftware.roadrunner-hexatonic.ts.net/api |
https://ecosoftware.roadrunner-hexatonic.ts.net/authentik/ |
| Local stack en tu equipo |
http://127.0.0.1:8095 |
http://127.0.0.1:9000/authentik/ |
En producción, Caddy elimina el prefijo /api antes de pasar la petición al
gateway. Por eso una misma ruta se escribe distinto según dónde estés:
https://ecosoftware.roadrunner-hexatonic.ts.net/api/v1/whoami ← producción http://127.0.0.1:8095/v1/whoami ← local
La documentación interactiva está deshabilitada a propósito.
/docs, /redoc y /openapi.json del gateway responden
404: la referencia es esta página, para no publicar la superficie del API a
cualquiera que la sondee.
Quickstart (5 minutos)
De credencial a primer dato, con curl. Sustituye
<CLIENT_ID> y <CLIENT_SECRET> por los tuyos
(ver cómo obtenerlos).
1. Pide un token
BASE=https://ecosoftware.roadrunner-hexatonic.ts.net TOKEN=$(curl -s -X POST $BASE/authentik/application/o/token/ \ -d grant_type=client_credentials \ -d client_id=<CLIENT_ID> \ -d client_secret=<CLIENT_SECRET> \ -d 'scope=openid eco_groups' | python -c "import sys,json;print(json.load(sys.stdin)['access_token'])")
2. Comprueba quién eres y qué puedes tocar
curl -s -H "Authorization: Bearer $TOKEN" $BASE/api/v1/whoami
{
"name": "ak-mi-app-client_credentials (authentik)",
"role": "oidc",
"grants": {
"s3": { "buckets": ["mi-bucket"], "perms": ["read", "write"] },
"mongo": { "databases": ["mi-app"], "collections": ["*"], "perms": ["read", "write"] }
}
}
Ese bloque grants es la verdad absoluta sobre lo que tu credencial puede
hacer. Si un servicio no aparece ahí, todas sus rutas responderán 403.
3. Escribe y lee un objeto
curl -s -X PUT -H "Authorization: Bearer $TOKEN" \
--data-binary "hola mundo" \
$BASE/api/v1/s3/mi-bucket/saludo.txt
curl -s -H "Authorization: Bearer $TOKEN" \
$BASE/api/v1/s3/mi-bucket/saludo.txt
Autenticación
Toda petición (salvo /v1/health) necesita la cabecera:
Authorization: Bearer <token>
El gateway acepta dos tipos de token y los distingue solo por su forma:
| Tipo | Cómo se ve | Se crea en | Para qué |
|---|---|---|---|
| JWT de Authentik recomendado |
eyJhbGciOi… |
Authentik (OAuth2 client credentials) | Aplicaciones y personas gobernadas por el IdP. Caduca solo y se renueva. |
| Token estático | ecok_… |
scripts/add-api-token.sh |
Máquinas o scripts sin IdP. No caduca; se revoca borrándolo del archivo. |
Flujo OAuth2 client_credentials
Es el flujo estándar de máquina a máquina: no hay usuario ni navegador de por medio. Tu aplicación cambia su par de credenciales por un token de vida corta.
curl -s -X POST https://ecosoftware.roadrunner-hexatonic.ts.net/authentik/application/o/token/ \ -d grant_type=client_credentials \ -d client_id=<CLIENT_ID> \ -d client_secret=<CLIENT_SECRET> \ -d 'scope=openid eco_groups'
{ "access_token": "eyJhbGciOi…", "token_type": "Bearer", "expires_in": 300 }
El scope eco_groups es obligatorio. Es el que mete tus grupos y tus
eco_grants dentro del token. Sin él, el token es válido pero llega sin
permisos: autenticado y con 403 en todo.
El token dura poco (por defecto 300 segundos). Tu cliente debe pedir uno
nuevo cuando caduque — el SDK de Python lo hace solo. Nunca guardes el
token en disco ni lo registres en logs; guarda únicamente el
client_id/client_secret, y en variables de entorno.
Tokens estáticos
Se generan en el servidor y se muestran una sola vez (solo se guarda su hash SHA-256). Sirven para cron jobs o integraciones sin IdP:
sh scripts/add-api-token.sh mi-cron service \
'{"s3":{"buckets":["mi-bucket"],"perms":["read"]}}'
El archivo de principales se lee en cada petición: añadir o revocar un token surte efecto de inmediato, sin reiniciar nada.
Obtener un client_id / client_secret
Cada aplicación tiene su propia credencial. Nunca se comparte una entre proyectos: así, revocar o rotar una no afecta a las demás, y los registros dicen quién hizo qué.
Opción A — un comando (recomendado)
Desde el repositorio de configuración, en el servidor:
ECO_AK_API=http://127.0.0.1:9001/authentik/api/v3 \ ECO_PUBLIC_BASE=https://ecosoftware.roadrunner-hexatonic.ts.net \ python3 scripts/new-api-client.py <nombre-app> <grupo-rol> <token-api-authentik>
Crea el proveedor OAuth2, la aplicación y su cuenta de servicio, la mete en el grupo que
elijas y te imprime el client_id y el client_secret listos para el
.env de tu aplicación. Es idempotente: repetirlo reutiliza lo que ya existe.
Opción B — a mano en la interfaz de Authentik
- Applications → Providers → Create → OAuth2/OIDC: tipo de cliente
Confidential, flujo de autorización implicit consent, y en
scopes selecciona
openid,email,profileyeco_groups. Al guardar te muestra elclient_idy elclient_secret. - Applications → Create: nombre y slug, con Provider = el que
acabas de crear. Este paso es obligatorio — sin una aplicación
enlazada, el flujo
client_credentialsfalla coninvalid_grant. - Directory → Users: busca la cuenta de servicio
ak-<proveedor>-client_credentials(Authentik la crea sola tras la primera petición de token) y dale permisos — con un grupo o coneco_grants, según la sección siguiente.
El client_secret es una contraseña. Va en el gestor de secretos o en
el .env de la aplicación (que está en .gitignore), nunca en el
código, ni en un chat, ni en un ticket. Si se filtra, se regenera desde el proveedor en
Authentik y la credencial anterior deja de servir.
Permisos y roles
Hay dos formas de decidir qué puede tocar una credencial. Se pueden combinar, y la segunda gana sobre la primera.
1. Grupos de rol (acceso amplio)
Meter la cuenta de servicio en uno de estos grupos de Authentik le da acceso a servicios completos:
| Grupo | Concede |
|---|---|
eco-admins | Todos los servicios, todos los recursos, lectura y escritura |
eco-data-write | Todos los servicios, lectura y escritura |
eco-data-read | Todos los servicios, solo lectura |
eco-s3 | Solo S3, lectura y escritura |
eco-pgadmin | Solo PostgreSQL, lectura y escritura |
eco-mongo | Solo MongoDB, lectura y escritura |
Sin ningún grupo de estos, la credencial se autentica correctamente pero no
tiene nada concedido: todas las llamadas responden 403.
2. Permisos por recurso con eco_grants (recomendado)
Para producción es mejor conceder exactamente lo que la aplicación necesita. Se escribe en el campo Attributes de la cuenta de servicio, en formato YAML:
eco_grants:
s3:
buckets: [mi-bucket]
perms: [read, write]
pg:
database: ecosoftware
tables: [facturas, clientes]
perms: [read]
mongo:
databases: [mi-app]
collections: ["*"]
perms: [read, write]
Cómo se leen estas reglas:
eco_grantssustituye a los permisos del grupo, no se suma a ellos.- Omitir un servicio lo deniega por completo. En el ejemplo de arriba,
cualquier ruta de Postgres distinta de
facturasoclientesda403, y escribir en Postgres también, porque solo tieneread. "*"significa "cualquier recurso de ese servicio".- Los permisos son solo dos:
readywrite. Cada endpoint exige uno u otro — está indicado en cada ficha de la referencia. - Un atributo puesto sobre un grupo aplica a todos sus miembros.
Cambiar permisos no requiere desplegar nada. Editas el campo Attributes (o mueves la cuenta de grupo) y el siguiente token que pida la aplicación ya viene con los permisos nuevos. Como los tokens duran 5 minutos, el cambio es efectivo casi al instante.
Referencia — Meta
Sonda de vida. Útil para monitoreo y para comprobar conectividad antes de depurar credenciales.
{ "ok": true }Devuelve tu identidad y tus permisos efectivos. Es la primera llamada que
debes hacer al depurar: si algo da 403, aquí ves exactamente qué
tiene concedido tu credencial. Nunca devuelve el token.
{
"name": "ak-mi-app-client_credentials (authentik)",
"role": "oidc",
"grants": { "s3": { "buckets": ["mi-bucket"], "perms": ["read","write"] } }
}Referencia — S3 (objetos)
Almacenamiento de objetos para todo lo grande: archivos, imágenes, respaldos, artefactos. El recurso que se controla es el bucket.
Lista los objetos del bucket. Parámetro opcional ?prefix= para filtrar por
carpeta lógica.
curl -s -H "Authorization: Bearer $TOKEN" \
"$BASE/api/v1/s3/mi-bucket?prefix=informes/2026/"{
"bucket": "mi-bucket",
"prefix": "informes/2026/",
"count": 2,
"objects": [
{ "key": "informes/2026/enero.pdf", "size": 84213, "last_modified": "2026-01-31T18:02:11Z" },
{ "key": "informes/2026/febrero.pdf", "size": 91002, "last_modified": "2026-02-28T17:44:03Z" }
]
}Genera un enlace temporal firmado para un objeto. Quien tenga la URL
puede descargarlo sin credenciales hasta que caduque (expires, por defecto
3600 segundos). Ideal para enviar un archivo a un usuario final o a un servicio externo sin
darle acceso al API.
{
"bucket": "mi-bucket",
"key": "informes/2026/enero.pdf",
"url": "https://…/mi-bucket/informes/2026/enero.pdf?X-Amz-Signature=…",
"expires": 3600
}Descarga los bytes del objeto. La respuesta no es JSON: es el contenido
crudo, con Content-Type: application/octet-stream y una cabecera
Content-Disposition con el nombre del archivo. La clave puede contener
barras (carpeta/sub/archivo.txt).
curl -s -H "Authorization: Bearer $TOKEN" \
"$BASE/api/v1/s3/mi-bucket/informes/2026/enero.pdf" -o enero.pdfSube o reemplaza un objeto. El cuerpo de la petición son los bytes del
archivo (no JSON, no multipart). Si envías Content-Type, se guarda
junto al objeto.
curl -s -X PUT -H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/pdf" \
--data-binary @enero.pdf \
"$BASE/api/v1/s3/mi-bucket/informes/2026/enero.pdf"{ "bucket": "mi-bucket", "key": "informes/2026/enero.pdf",
"size": 84213, "etag": "e1dde0300e506e4a9430215483f3a3e7" }Borra un objeto.
{ "deleted": true, "bucket": "mi-bucket", "key": "informes/2026/enero.pdf" }Referencia — PostgreSQL (tablas)
CRUD sobre las tablas de la base de datos ecosoftware. El recurso que se
controla es la tabla. Las consultas se construyen de forma parametrizada
(identificadores citados, valores enlazados), así que no hay inyección SQL posible por esta
vía.
Alcance deliberado. Este API cubre CRUD por igualdad de columnas. No hay JOINs,
agregaciones ni SQL libre: para eso conéctate de forma nativa con
EcoDB.pg por la VPN.
Devuelve filas. Acepta where (objeto JSON de igualdades unidas por AND) y
limit, ya sea como parámetros de consulta o en el cuerpo JSON. Si van en
ambos, gana el parámetro de consulta.
curl -s -H "Authorization: Bearer $TOKEN" \
"$BASE/api/v1/pg/facturas?where=%7B%22estado%22%3A%22pendiente%22%7D&limit=50"El where va codificado como URL: {"estado":"pendiente"}.
{
"table": "facturas",
"count": 2,
"rows": [
{ "id": 41, "cliente": "ACME", "total": 1520.00, "estado": "pendiente" },
{ "id": 43, "cliente": "Globex", "total": 890.50, "estado": "pendiente" }
]
}Inserta una fila. El cuerpo es el objeto {columna: valor}. Devuelve la fila
insertada completa, incluidos los valores generados por la base (como el id).
curl -s -X POST -H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"cliente":"ACME","total":1520.00,"estado":"pendiente"}' \
"$BASE/api/v1/pg/facturas"{ "table": "facturas",
"inserted": { "id": 44, "cliente": "ACME", "total": 1520.00, "estado": "pendiente" } }Actualiza filas. El cuerpo lleva values (lo que se escribe) y
where (a qué filas). Devuelve cuántas filas cambiaron.
curl -s -X PATCH -H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"values":{"estado":"pagada"},"where":{"id":44}}' \
"$BASE/api/v1/pg/facturas"{ "table": "facturas", "updated": 1 }Protección: where no puede ir vacío. Una
actualización sin filtro se rechaza con 400 para no arrasar la tabla entera.
Borra filas. El cuerpo lleva where, que tampoco puede ir vacío.
curl -s -X DELETE -H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"where":{"id":44}}' \
"$BASE/api/v1/pg/facturas"{ "table": "facturas", "deleted": 1 }Referencia — MongoDB (documentos)
Para datos sin esquema fijo: eventos, registros, documentos anidados. El recurso que se controla es el par (base de datos, colección).
Busca documentos. ?filter= acepta cualquier filtro de Mongo en JSON
(codificado como URL) y ?limit= acota el resultado —
limit=0 significa sin límite, que es el valor por defecto.
curl -s -H "Authorization: Bearer $TOKEN" \
"$BASE/api/v1/mongo/mi-app/eventos?filter=%7B%22tipo%22%3A%22alta%22%7D&limit=20"{
"database": "mi-app",
"collection": "eventos",
"count": 1,
"documents": [ { "_id": "6a6eea53f919cbc230b3406e", "tipo": "alta", "ok": true } ]
}Los ObjectId se devuelven como texto para que sean JSON válido.
Inserta un documento. El cuerpo es el documento tal cual.
curl -s -X POST -H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"tipo":"alta","usuario":"maria","ok":true}' \
"$BASE/api/v1/mongo/mi-app/eventos"{ "database": "mi-app", "collection": "eventos",
"inserted_id": "6a6eea53f919cbc230b3406e" }Actualiza documentos. El cuerpo lleva filter, update (con
operadores de Mongo como $set) y opcionalmente
many: true para afectar a más de uno (por defecto solo al primero).
curl -s -X PATCH -H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"filter":{"tipo":"alta"},"update":{"$set":{"revisado":true}},"many":true}' \
"$BASE/api/v1/mongo/mi-app/eventos"{ "database": "mi-app", "collection": "eventos",
"matched": 3, "modified": 3, "upserted_id": null }Protección: update no puede ir vacío —
usa operadores ($set, $inc, …), no un documento suelto.
Borra documentos que coincidan con filter; con many: true
borra todos los que coincidan.
curl -s -X DELETE -H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"filter":{"tipo":"alta"},"many":false}' \
"$BASE/api/v1/mongo/mi-app/eventos"{ "database": "mi-app", "collection": "eventos", "deleted": 1 }Protección: filter no puede ir vacío — un
borrado sin filtro se rechaza con 400.
Errores
Todos los errores llegan como JSON con la misma forma, y el código HTTP dice de quién es el problema:
{ "error": "principal 'mi-app' is not allowed to write s3 resource 'otro-bucket'" }| Código | Qué significa | Qué hacer |
|---|---|---|
| 400 | Petición mal formada: falta el cuerpo, o un
where/filter/update obligatorio va vacío. |
Revisa el cuerpo JSON contra la ficha del endpoint. |
| 401 | Falta la cabecera Authorization, está mal
formada, o el token es inválido o caducó. |
Pide un token nuevo. Verifica que envías Bearer <token>. |
| 403 | Estás autenticado pero el recurso está fuera de tus permisos. | Llama a /v1/whoami y compara con lo que intentas hacer. Si falta algo,
ajusta eco_grants o el grupo. |
| 404 | El objeto, la fila o la ruta no existe. | Revisa bucket/clave, o si estás usando la base correcta. |
| 422 | Parámetros de la petición inválidos (por ejemplo un
limit que no es número). | Corrige los parámetros de la URL. |
| 500 | El gateway está mal configurado. | No es culpa de tu cliente: avisa a quien opera la plataforma. |
| 502 | Falló el motor de datos por debajo (S3, Postgres o Mongo). | Reintenta con espera; si persiste, avisa a operación. |
Diagnóstico rápido de un 403
Es el error más común y casi siempre tiene la misma causa. Comprueba en este orden:
/v1/whoami— ¿aparece el servicio (s3,pg,mongo) engrants? Si no aparece, está denegado entero.- ¿El recurso exacto (bucket, tabla, colección) está en la lista, o hay
"*"? - ¿El permiso que necesitas (
readowrite) está enperms? Escribir con soloreadda 403. - Si
grantssale vacío, casi seguro pediste el token sin el scopeeco_groups.
SDK de Python — ecodbapi
Si tu aplicación es Python, el SDK evita escribir el manejo de tokens y de HTTP a mano. Solo necesita la biblioteca estándar para la parte del API.
pip install ecodbapi
Dos formas de conectarse
| Clase | Cómo conecta | Cuándo usarla |
|---|---|---|
EcoAPI | Por HTTPS a través del gateway, con
client_id/client_secret |
Desde cualquier red, sin VPN. Es el equivalente en Python a todo lo de esta página. |
EcoDB | Directo a S3/Postgres/Mongo con protocolos nativos | Dentro de la VPN, para ingesta masiva o consultas pesadas. |
EcoAPI — a través del gateway
Pide el token en la primera llamada y lo renueva solo antes de que caduque; nunca lo
registra en logs ni lo expone en repr().
import os
from ecodbapi import EcoAPI
api = EcoAPI(
os.environ["ECO_CLIENT_ID"],
os.environ["ECO_CLIENT_SECRET"],
base_url="https://ecosoftware.roadrunner-hexatonic.ts.net",
)
print(api.whoami()) # identidad y permisos efectivos
# --- S3 ---
api.s3_put("mi-bucket", "informes/enero.pdf", open("enero.pdf", "rb").read())
datos = api.s3_get("mi-bucket", "informes/enero.pdf")
url = api.s3_url("mi-bucket", "informes/enero.pdf", expires=600) # enlace temporal
api.s3_list("mi-bucket", prefix="informes/")
api.s3_delete("mi-bucket", "informes/enero.pdf")
# --- PostgreSQL ---
api.pg_insert("facturas", {"cliente": "ACME", "total": 1520.00, "estado": "pendiente"})
filas = api.pg_select("facturas", where={"estado": "pendiente"}, limit=50)
api.pg_update("facturas", values={"estado": "pagada"}, where={"id": 44})
api.pg_delete("facturas", where={"id": 44})
# --- MongoDB ---
api.mongo_insert("mi-app", "eventos", {"tipo": "alta", "usuario": "maria"})
docs = api.mongo_find("mi-app", "eventos", filter={"tipo": "alta"}, limit=20)
api.mongo_update("mi-app", "eventos", {"tipo": "alta"}, {"$set": {"revisado": True}})
api.mongo_delete("mi-app", "eventos", {"tipo": "alta"})Manejo de errores
Los códigos HTTP se traducen a excepciones, todas descendientes de
EcoDBError:
from ecodbapi import EcoAPI
from ecodbapi.errors import PermissionDeniedError, NotFoundError, EcoDBError
try:
api.s3_put("bucket-ajeno", "x.txt", b"...")
except PermissionDeniedError:
... # 403: fuera de los permisos de esta credencial
except NotFoundError:
... # 404
except EcoDBError as e:
... # cualquier otro fallo del API o del motorLa guía completa del SDK, con la ruta directa por VPN, vive en el
repositorio platform-server-databases-configuration-api
(docs/INTEGRATION.md).
Recetas
El mismo flujo — pedir token y usarlo — en los lenguajes más habituales.
import json, urllib.parse, urllib.request
BASE = "https://ecosoftware.roadrunner-hexatonic.ts.net"
def token(client_id, client_secret):
body = urllib.parse.urlencode({
"grant_type": "client_credentials",
"client_id": client_id, "client_secret": client_secret,
"scope": "openid eco_groups"}).encode()
req = urllib.request.Request(f"{BASE}/authentik/application/o/token/", data=body)
with urllib.request.urlopen(req, timeout=30) as r:
return json.load(r)["access_token"]
t = token(CLIENT_ID, CLIENT_SECRET)
req = urllib.request.Request(f"{BASE}/api/v1/whoami",
headers={"Authorization": f"Bearer {t}"})
with urllib.request.urlopen(req, timeout=30) as r:
print(json.load(r))const BASE = "https://ecosoftware.roadrunner-hexatonic.ts.net";
async function getToken(clientId, clientSecret) {
const r = await fetch(`${BASE}/authentik/application/o/token/`, {
method: "POST",
headers: { "Content-Type": "application/x-www-form-urlencoded" },
body: new URLSearchParams({
grant_type: "client_credentials",
client_id: clientId, client_secret: clientSecret,
scope: "openid eco_groups",
}),
});
if (!r.ok) throw new Error(`token: HTTP ${r.status}`);
return (await r.json()).access_token;
}
const token = await getToken(CLIENT_ID, CLIENT_SECRET);
const res = await fetch(`${BASE}/api/v1/mongo/mi-app/eventos`, {
method: "POST",
headers: { Authorization: `Bearer ${token}`, "Content-Type": "application/json" },
body: JSON.stringify({ tipo: "alta", usuario: "maria" }),
});
console.log(await res.json());Nunca en el navegador del usuario final. El
client_secret no puede viajar a código público: úsalo solo en tu backend.
BASE=https://ecosoftware.roadrunner-hexatonic.ts.net TOKEN=$(curl -s -X POST $BASE/authentik/application/o/token/ \ -d grant_type=client_credentials \ -d client_id="$ECO_CLIENT_ID" -d client_secret="$ECO_CLIENT_SECRET" \ -d 'scope=openid eco_groups' | jq -r .access_token) curl -s -H "Authorization: Bearer $TOKEN" $BASE/api/v1/whoami | jq
Subir un archivo y compartirlo con un enlace temporal
from ecodbapi import EcoAPI
api = EcoAPI(CLIENT_ID, CLIENT_SECRET,
base_url="https://ecosoftware.roadrunner-hexatonic.ts.net")
with open("reporte.pdf", "rb") as f:
api.s3_put("mi-bucket", "reportes/2026/marzo.pdf", f.read())
# enlace válido 10 minutos, se puede enviar por correo a alguien sin credenciales
print(api.s3_url("mi-bucket", "reportes/2026/marzo.pdf", expires=600))Registrar eventos de una aplicación en Mongo
from datetime import datetime, timezone
api.mongo_insert("mi-app", "eventos", {
"tipo": "despliegue",
"version": "1.4.2",
"ts": datetime.now(timezone.utc).isoformat(),
})
recientes = api.mongo_find("mi-app", "eventos", filter={"tipo": "despliegue"}, limit=10)
for doc in recientes["documents"]:
print(doc["ts"], doc["version"])Límites y buenas prácticas
Lo que este API no hace
- SQL libre — solo CRUD por igualdad de columnas. Para JOINs y agregaciones, conexión nativa.
- Subidas multiparte — el
PUTmanda el archivo completo en el cuerpo. Para archivos muy grandes o ingesta masiva, usa la ruta nativa por VPN. - Transacciones entre varias llamadas — cada petición es independiente.
- Paginación con cursor — hay
limity, en S3,prefix.
Recomendaciones
- Reutiliza el token mientras siga vigente; no pidas uno por cada petición. El SDK ya lo gestiona.
- Concede lo mínimo con
eco_grants: una credencial por aplicación, limitada a sus buckets y colecciones. - Guarda las credenciales en variables de entorno, nunca en el repositorio.
- Rota el
client_secretante cualquier sospecha: se regenera en Authentik sin tocar las bases de datos. - Maneja el 502 con reintentos y espera creciente; el resto de errores no se arreglan reintentando.
- Los
whereyfiltervacíos están prohibidos a propósito en actualizaciones y borrados: es una red de seguridad, no un obstáculo.
Preguntas frecuentes
¿Necesito VPN para usar el API?
No. El dominio de la plataforma es accesible desde internet y el API está protegido por token. La VPN solo hace falta para conexiones nativas a los puertos de las bases de datos.
¿Por qué mi token funciona pero todo da 403?
Casi siempre porque se pidió sin el scope eco_groups, o porque la cuenta de
servicio no está en ningún grupo ni tiene eco_grants. Compruébalo con
/v1/whoami: si grants viene vacío, es eso.
¿Cuánto dura un token?
Unos 5 minutos (expires_in en la respuesta). Pide otro cuando caduque; el SDK
lo renueva automáticamente.
¿Puedo usar el API desde el navegador de un usuario?
No con el client_secret: eso lo expondría a cualquiera. Haz las llamadas
desde tu backend, y si el usuario debe descargar un archivo, entrégale un
enlace temporal firmado.
¿Cómo pido acceso a un bucket o tabla nueva?
Se añade el recurso a los eco_grants de tu cuenta de servicio (o se crea la
vía correspondiente si aún no existe). Es un cambio de configuración, sin desplegar código, y
aplica en el siguiente token.
¿Qué pasa si se cae una base de datos?
Las rutas de ese motor devuelven 502; las de los otros dos siguen
funcionando. /v1/health solo indica que el gateway está vivo.
¿Dónde está el código de todo esto?
El servicio (gateway) vive en el repositorio de configuración,
ecodb-gateway/, junto al stack que lo despliega. El SDK y sus
guías viven en platform-server-databases-configuration-api
(pip install ecodbapi).