Skip to content

gRPC API

Requires a license with the grpc feature. See pricing.

The gRPC API serves your content types and entries to gRPC clients on a port of its own, with the same data, tenants, roles and data residency rules as the REST APIs. A second port answers health checks and offers every method as a plain JSON route, which is handy with curl. It adds no features over REST: it carries the same calls over HTTP/2.

ListenerVariableDefaultServes
gRPCGRPC_ADDR127.0.0.1:3003SchemaService, ContentService, FlowService, gRPC health, and reflection outside production
Health and JSONGRPC_HEALTH_ADDR127.0.0.1:3004/healthz, and every method as a JSON route under /api/

Both are separate from the Admin API (3001) and the Content API (3002). Both default to loopback, so nothing outside the container reaches them until you change the address. The gRPC listener is plaintext HTTP/2: put TLS in front of it as you do for the Content API. The Kubernetes deployment shows the container ports and Service entries.

You need a signed-in user's token in TOKEN. The quickstart shows how to get one.

  1. Open both listeners and publish the ports (the engine image exposes 3003 and 3004), then restart:

    Terminal window
    GRPC_ADDR=0.0.0.0:3003
    GRPC_HEALTH_ADDR=0.0.0.0:3004
  2. Check health, which needs no token:

    Terminal window
    curl http://localhost:3004/healthz
    {"status": "SERVING", "services": {"": "SERVING", "lyeve.core.v1.ContentService": "SERVING", "lyeve.core.v1.SchemaService": "SERVING"}}
  3. List entries of a content type through the JSON route:

    Terminal window
    curl http://localhost:3004/api/content/post \
    -H "Authorization: Bearer $TOKEN"
    {"items": [{"id": "c8a14e2b-7d3f-4b90-a1c6-5e8f0d2b9a74", "title": "Hello"}]}
  4. Call the gRPC listener itself with grpcurl. Reflection lets grpcurl discover the services, and it is served only when APP_ENV is not production:

    Terminal window
    grpcurl -plaintext \
    -H "authorization: Bearer $TOKEN" \
    localhost:3003 lyeve.core.v1.SchemaService/ListSchemas

    The answer lists your content types as schemas, each with name and definition.

  5. Open Delivery > gRPC in the admin console as a super admin. It shows the listeners, their services and health, and runs one JSON route for you.

ServiceMethodsRole
lyeve.core.v1.SchemaServiceListSchemas, GetSchemaviewer, editor or admin
lyeve.core.v1.ContentServiceList, Getviewer, editor or admin
lyeve.core.v1.ContentServiceCreate, Update, Deleteeditor or admin
lyeve.core.v1.FlowServiceRun, Listany signed-in user, and the flow's own auth setting
grpc.health.v1.HealthCheck, Watchnone
grpc.reflection.v1.ServerReflectiona token, and only outside production

A super admin may call every method. The services are defined in schema.proto, content.proto and flow.proto, package lyeve.core.v1.

Messages travel as JSON, so you can call the services without generated message types: use the json content subtype, which sends Content-Type: application/grpc+json. In Go, pass grpc.CallContentSubtype("json"). The .proto files name some fields with a _json suffix (definition_json, items_json, data_json, item_json). On the wire the keys are definition, items, data and item, and their values are JSON objects rather than encoded strings.

MethodRequestAnswer
ContentService.Listschema, limit (default 50, 1 to 500), offset, filters (equality on known fields), localeitems
ContentService.Getschema, id, localeitem
ContentService.Createschema, dataitem
ContentService.Updateschema, id, dataitem
ContentService.Deleteschema, idempty
SchemaService.ListSchemasnoneschemas, each name and definition
SchemaService.GetSchemanamename and definition
FlowService.Runslug, inputrun_id, status, headers, body
FlowService.Listnoneflows, each slug, name, description, auth and input_schema

On content types that use soft delete, List and Get leave deleted entries out, and Delete soft-deletes.

A content type's transports block decides what gRPC serves:

CaseAnswer
The content type is off for gRPCNOT_FOUND, the answer a missing content type gets, and ListSchemas leaves it out
It is served, but not in this direction (r refuses writes, w refuses reads)FAILED_PRECONDITION

See Transports.

Send a signed-in user's session token in the authorization metadata on gRPC, or the Authorization header on the JSON routes. API keys are not accepted here, and neither is a sign-in that still waits for its second factor. Only the health service and /healthz answer without a token. The token's tenant_id claim decides the tenant, never a header the client sends. See Tenants.

Every call is rate limited, authenticated, scoped to its tenant and checked for data residency before it runs.

The health and JSON listener offers the same calls for clients without gRPC:

JSON routes on GRPC_HEALTH_ADDR
MethodPathSame as
GET/healthzHealth summary. ?service=<name> checks one service. No token needed.
GET/api/schemasSchemaService.ListSchemas
GET/api/schemas/{name}SchemaService.GetSchema
GET/api/content/{schema}ContentService.List. Takes locale.
GET/api/content/{schema}/{id}ContentService.Get. Takes locale.
POST/api/content/{schema}ContentService.Create. Answers 201.
PUT/api/content/{schema}/{id}ContentService.Update
DELETE/api/content/{schema}/{id}ContentService.Delete. Answers {"deleted": "<id>"}.
GET/api/flowsFlowService.List
POST/api/flows/{slug}FlowService.Run. The body is the input.

GET / redirects to /healthz.

On these routes the whole body is the entry, while the Content API on 3002 takes {"data": {...}}. Sending that envelope here creates an entry with one field named data:

Terminal window
curl -X POST http://localhost:3004/api/content/post \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"title": "From gRPC JSON", "slug": "from-grpc-json"}'

The answer is 201 with {"item": {...}}. A body is at most 1 MB, and one that is not JSON answers 400 with {"error": "invalid JSON body"}.

When flows run beside gRPC, FlowService.Run starts a published flow of the caller's tenant whose HTTP trigger lists grpc in protocols, with input as the trigger body:

Terminal window
grpcurl -plaintext \
-H "authorization: Bearer $TOKEN" \
-d '{"slug": "order-totals", "input": {"order": 7}}' \
localhost:3003 lyeve.core.v1.FlowService/Run

The answer carries the run id (empty when the flow's cache answered), the status the flow set or 200, its headers and the body. The flow's auth setting decides who may run it: public and auth flows answer, and admin flows answer NOT_FOUND. Its rate limit and cache count gRPC callers apart from REST and GraphQL callers.

CodeMeaning
NOT_FOUNDNo published flow has that slug for this protocol and caller.
PERMISSION_DENIEDThe license does not carry flow-pro, which starting a flow over gRPC needs.
RESOURCE_EXHAUSTEDThe flow's rate limit refused the call.
ABORTEDThe call would nest flows too deep.
UNAVAILABLEThe flow could not run.
UNIMPLEMENTEDFlows are not available on this instance. The JSON routes answer 501.

Both need a super admin and live on the Admin API.

MethodPathPurpose
GET/api/admin/grpc/statusThe listeners, their services, what health reports and the JSON routes.
POST/api/admin/grpc/invokeRun one JSON route inside the engine with your own token and return its answer.
Terminal window
curl -X POST http://localhost:3001/api/admin/grpc/invoke \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"route": "GET /api/content/{schema}", "params": {"schema": "post"}}'

route is one of the JSON routes, written as in the table. params fills its {name} parts and body is sent on a write. The answer carries the status, duration_ms, headers and body.

VariableWhat it doesDefault
GRPC_ADDRAddress of the gRPC listener127.0.0.1:3003
GRPC_HEALTH_ADDRAddress of the health and JSON listener127.0.0.1:3004
GRPC_RATE_LIMITCalls a second per gRPC method, shared by every caller. Only a positive number is taken, so the limit cannot be turned off.100
GRPC_RATE_BURSTBurst per gRPC method200
JWT_SECRET, JWT_SECRETSThe secrets tokens are checked against. Without one, every call except health is refused.none
APP_ENVOutside production, server reflection is served. Nothing else changes.production
INSTANCE_REGIONWith data residency, writes from a tenant assigned to another region are refused.empty, so off

A gRPC message is at most 32 MiB each way, and calls in flight get up to 10 seconds to finish on shutdown. If you set LYEVE_PLUGINS to choose which features start, include grpc. See Licensing and tiers.

On the JSON routes, gRPC codes map to HTTP statuses.

gRPC codeHTTPCause
UNAUTHENTICATED401No token, a token that does not verify, or no JWT secret configured.
PERMISSION_DENIED403The caller's role does not allow it, or FlowService without flow-pro.
INVALID_ARGUMENT400A malformed request.
NOT_FOUND404No such content type or entry.
ALREADY_EXISTS409A conflicting entry.
RESOURCE_EXHAUSTED429rate limit exceeded. Slow down or raise GRPC_RATE_LIMIT.
FAILED_PRECONDITION421A write from a tenant that belongs in another region. The JSON answer is {"error": "write routed to wrong region", "target_region": "<slug>"}.
FAILED_PRECONDITION500A content type served over gRPC in the other direction only. The JSON routes answer 500 with {"error": "Internal Server Error"} today, where gRPC itself names the reason.
DEADLINE_EXCEEDED504The call ran out of time.
UNAVAILABLE503The tenant's region could not be read, or the database did not answer.