GraphQL API
Elido ships a GraphQL endpoint alongside the REST API. Every GraphQL field is implemented as a thin pass-through to the same api-core endpoints the REST surface uses, so anything you can do over GraphQL you can also do over REST — pick whichever fits the shape of your screen.
The endpoint is POST /graphql, hosted on the graphql-gateway
service. Authentication is identical to REST: send your API token
or OAuth2 access token in the Authorization: Bearer <token>
header.
This is the foundation drop. Subscriptions (real-time clicks), persisted queries, federation, and response caching are all on the roadmap — see ADR-0031 for the full deferred-work list.
When to use GraphQL vs REST
- Use GraphQL when a screen needs 4+ resources composed together (workspace + links + recent clicks + top-N) — collapse the round-trips into one query shaped to the screen.
- Use GraphQL for mobile screens where bytes and round-trips matter on flaky networks.
- Use REST + the SDK for everything else. Webhooks, bulk ingestion, file uploads, and admin tooling are all REST-first and will stay that way.
Authentication
curl -X POST https://api.elido.app/graphql \
-H "Authorization: Bearer $ELIDO_TOKEN" \
-H "Content-Type: application/json" \
-d '{"query": "{ me { id email } }"}'ek_… API tokens (Personal access tokens, workspace machine
tokens) and OAuth2 access tokens both work. Public introspection
queries like { __schema { types { name } } } work without a
token in development; production deploys gate introspection
behind GRAPHQL_INTROSPECTION=true.
Examples
1. Dashboard landing — current user + workspace list
query DashboardLanding {
me {
id
email
fullName
timezone
}
workspaces {
id
name
role
planTier
}
}One round-trip, two REST endpoints folded into one response. The mobile app uses this as the first authenticated query after login.
2. Workspace links — Relay-style pagination
query WorkspaceLinks($workspaceId: ID!, $after: String) {
links(workspaceId: $workspaceId, first: 50, after: $after) {
edges {
node {
id
slug
destinationUrl
title
status
tags
createdAt
}
}
pageInfo {
endCursor
hasNextPage
}
}
}Pass pageInfo.endCursor from the previous response back as the
next after to fetch the next page. hasNextPage is false
when you’ve reached the end. Cursors are opaque — don’t decode
them.
3. Analytics overview — timeseries + top links in one shot
query Overview(
$workspaceId: ID!
$from: DateTime!
$to: DateTime!
) {
timeseries(
workspaceId: $workspaceId
from: $from
to: $to
interval: "day"
) {
ts
count
}
topLinks(
workspaceId: $workspaceId
from: $from
to: $to
limit: 10
) {
slug
clicks
}
}from/to are ISO-8601 timestamps. interval accepts "hour"
or "day"; api-core validates and returns an error on anything
else. The dashboard’s overview screen issues this exact query —
GraphQL collapses what used to be two REST calls into one.
4. Mutations — create a short link
mutation CreateLink($input: CreateLinkInput!) {
createLink(input: $input) {
id
slug
destinationUrl
createdAt
}
}Variables:
{
"input": {
"workspaceId": "1",
"destinationUrl": "https://example.com/launch",
"tags": ["marketing", "q4"]
}
}updateLink(workspaceId, linkId, input) and
deleteLink(workspaceId, linkId) round out the link mutations.
Mirroring REST, mutations are idempotent on the server side —
the gateway forwards the SDK’s auto-generated Idempotency-Key
header.
What’s not in this drop
These are intentional omissions for the foundation phase. Each will land as its own update with a migration note:
- Subscriptions — real-time click events. WebSocket transport,
bridges to Redis where analytics-api already publishes
clicks:link:<id>. - Persisted queries — production builds will ship a manifest hashed against trusted clients only. Today, ad-hoc queries are fine.
- Federation — analytics and billing are good candidates to own their own subgraphs eventually.
Limits and conventions
- Same per-token rate limits as REST (advertised via the
X-RateLimit-*headers on the GraphQL response). - Errors follow GraphQL’s spec — top-level
errorsarray; partial data may still be present indata. - IDs are stringified (
"1", not1) to keep BigInt-safe IDs consistent across the schema.