API pública
Un único endpoint GraphQL para todo lo que hacen el panel, la CLI y el servidor MCP. Tipos de token, límites de tasa, operaciones comunes y compatibilidad con Railway.
POST https://api.skiffly.dev/graphql (alias: /graphql/v2)
Authorization: Bearer <token>
Content-Type: application/jsonGraphiQL está disponible en la misma URL desde un navegador. El esquema sigue la API pública de Railway: conexiones Relay, objetos input, DeploymentStatus en mayúsculas, variables como un mapa, así que los documentos escritos para Railway suelen ejecutarse sin cambios.
Tokens#
| Token | Encabezado | Alcance | Crear |
|---|---|---|---|
| Cuenta | Authorization: Bearer <session token> | todos los espacios de trabajo del usuario | skiffly login guarda uno |
| Espacio de trabajo | Authorization: Bearer skiffly_… | un espacio de trabajo; scope read o write | Configuración → Desarrollador → Crear token, o apiTokenCreate(input: { name, workspaceId, scopes }) |
| Proyecto | Project-Access-Token: skiffly_project_… | un entorno de un proyecto: desplegar, variables, logs, dominios, volúmenes | projectTokenCreate(input: { projectId, environmentId, name }) |
Los tokens se muestran una sola vez. Los tokens de proyecto son la opción correcta para CI: no pueden ver otros proyectos ni la facturación. Cualquier cosa fuera del alcance del token devuelve FORBIDDEN.
Límites de tasa por plan del espacio de trabajo: Free 100 solicitudes/hora; Hobby 1 000/hora y 10/segundo; Pro 10 000/hora y 50/segundo. Las respuestas incluyen X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Reset; un 429 incluye Retry-After.
Un helper para el shell#
export SKIFFLY_TOKEN=skiffly_...
gql() { curl -s https://api.skiffly.dev/graphql -H "Authorization: Bearer $SKIFFLY_TOKEN" -H 'content-type: application/json' \
--data-binary "$(node -e 'const [q,v]=process.argv.slice(1);process.stdout.write(JSON.stringify({query:q,variables:JSON.parse(v||"{}")}))' "$1" "$2")"; echo; }
gql '{ me { login workspaces { id name role } } }'La CLI hace lo mismo con skiffly api '<query>' -v '<json>' usando su token guardado.
Recorrido: proyecto → servicio → dominio#
gql 'mutation($ws:String!){ projectCreate(input:{workspaceId:$ws, name:"wevo"}){ id environments { edges { node { id name } } } } }' '{"ws":"<workspaceId>"}'
# repositorio (Railpack, o el Dockerfile en la raíz)
gql 'mutation{ serviceCreate(input:{projectId:"<projectId>", name:"web", source:{repo:"acme/web"}, branch:"main"}){ id } }'
# imagen, con variables
gql 'mutation{ serviceCreate(input:{projectId:"<projectId>", name:"redis", source:{image:"redis:7"}, port:6379, variables:{ REDIS_ARGS:"--save 60 1" }}){ id } }'
# variables (serviceId null = compartidas); los valores pueden referenciar otros servicios
gql 'mutation{ variableCollectionUpsert(input:{projectId:"<projectId>", environmentId:"<envId>", serviceId:"<serviceId>", variables:{ REDIS_URL:"${{redis.REDIS_URL}}" }}) }'
gql '{ variables(projectId:"<projectId>", environmentId:"<envId>", serviceId:"<serviceId>") }'
# dominios
gql 'mutation{ serviceDomainCreate(input:{serviceId:"<serviceId>", environmentId:"<envId>"}){ domain } }'
gql 'mutation{ customDomainCreate(input:{projectId:"<projectId>", serviceId:"<serviceId>", environmentId:"<envId>", domain:"www.example.com"}){ id status { dnsRecords { recordType fqdn requiredValue } } } }'
gql 'mutation{ customDomainVerify(id:"<domainId>"){ status { verified } } }'
# volumen, configuración, despliegue, logs
gql 'mutation{ volumeCreate(input:{projectId:"<projectId>", serviceId:"<serviceId>", environmentId:"<envId>", mountPath:"/data", sizeGb:5}){ id } }'
gql 'mutation{ serviceInstanceUpdate(serviceId:"<serviceId>", environmentId:"<envId>", input:{ memoryBytes:1073741824, cpuMillis:1000, healthcheckPath:"/healthz", numReplicas:1 }) }'
gql 'mutation{ serviceInstanceDeployV2(serviceId:"<serviceId>", environmentId:"<envId>") }'
gql '{ deployments(input:{serviceId:"<serviceId>", environmentId:"<envId>"}, first:3){ edges { node { id status message createdAt } } } }'
gql '{ deploymentLogs(deploymentId:"<deploymentId>", limit:200){ timestamp severity message } }'serviceInstanceDeployV2 devuelve el id del despliegue; consulta deployment(id) { status message } hasta que sea SUCCESS o FAILED.
Errores#
Los errores de GraphQL llevan extensions.code: UNAUTHENTICATED, FORBIDDEN, NOT_FOUND, BAD_USER_INPUT, CONFLICT, PLAN_LIMIT (con el límite alcanzado), TEMPLATE_VARIABLES_REQUIRED (con extensions.missing), RATE_LIMITED.
Compatibilidad con Railway#
Mismos nombres y formas donde la semántica coincide: project, projects, environment(s), service, serviceInstance, deployment(s), deploymentLogs, environmentLogs, variables, domains, customDomain*, serviceDomain*, volume*, projectToken*, apiToken*, templates, template, templateDeployV2, serviceInstanceUpdate, serviceInstanceDeployV2, serviceInstanceRedeploy, variableUpsert, variableCollectionUpsert, variableDelete, deploymentCancel, deploymentRedeploy, deploymentRestart, deploymentRollback, tcpProxy*. Los argumentos son ID!, pero se aceptan documentos que declaran $id: String!.
Diferencias que importan:
| Railway | Skiffly |
|---|---|
buildLogs y deploymentLogs | un solo deploymentLogs; Log.stream es build, runtime o system |
deploymentRestart(id) | también serviceRestart(serviceId, environmentId) |
serviceConnect para adjuntar un origen | serviceUpdate(id, { source }) |
| los recursos se definen a nivel de plan | serviceInstanceUpdate acepta cpuMillis / memoryBytes |
objeto Workspace.plan | enum Plan FREE / HOBBY / PRO |
| Subscriptions para los logs | solo HTTP; consulta deploymentLogs periódicamente |
Cambios preparados (environmentStageChanges, environmentPatchCommit*) | no existen: los cambios se aplican de inmediato; vuelve a desplegar cuando haga falta |
| Regiones, gateways de salida, redes privadas, buckets, funciones, sandboxes, equipos/RBAC, feature flags, webhooks | no disponibles |
Adiciones exclusivas de Skiffly: billingAccount, ledger, payments, topupCreate, usage, serviceMetrics, nodes, githubInstallations, githubApp, tcpProxies, Service.internalHost, ServiceInstance.branch, ServiceUpdateInput.runtimeClass.
El SDL completo se sirve por introspección en el endpoint; la referencia de GraphQL lista todos los campos raíz.