Troubleshooting
Common problems and how to diagnose them
Facts Are Not Being Created
Fact creation is all-or-nothing per request: if any fact in a
Fact.createAll() batch is not authorized, the server rejects the whole
batch with 403 and the SDK resolves to an empty array instead of
throwing. Always check the return value:
The most common causes:
-
The term was never declared. Before using a term in an
isAfact, it must be declared with$isATermFor- see Terms: -
You lack permission on the subject or object. To use a record as a fact's subject you need
$canRefine(or stronger) on it; to use it as an object you need$canReferTo(or stronger). See the authorization model. -
Self-invitation. Users cannot add themselves to a team with
$isMemberOf- a creator or host must add them (see Team Patterns).
Queries Return No Results
- Wrong data type name.
$hasDataTypeexpects the legacy type stringsKeyValueAttribute,LongTextAttribute,BlobAttribute(or the SDK classesKeyValueRecordetc. in TypeScript) - not"KeyValueRecord"as a string. - The records aren't shared with this user. Queries only return records
the current user can access. Verify with the record owner's session, and
check the facts granting access (
$canRead,$canAccess,$isMemberOfchains). - State filters. When using
['$it', '$latest(stateIs)', someStateId], remember$latestonly matches the most recentstateIsfact - an older matching fact does not count.
Login Problems
- Redirect URI mismatch. The redirect URI registered at your OIDC
provider must match the SDK's
redirect_uriexactly (scheme, host, port, path). - Login works, but requests are 401. In public client mode the server
needs
ALLOW_HTTP_AUTHENTICATION_HEADER=trueandAUTH_TOKEN_AUDIENCEmatching the token'saudclaim. - Session never sticks across domains. Confidential client mode uses an HttpOnly cookie, which browsers treat as a third-party cookie when the frontend and the LinkedRecords server are on different domains - use public client mode instead. See Configuration.
CORS Errors
If the browser console shows CORS failures:
- Set
CORS_ORIGINto a JSON array containing your frontend origin, e.g.CORS_ORIGIN='["https://your-app.com"]'- or leave it unset and setFRONTEND_BASE_URLto that origin. - Origins must match exactly (scheme, host, port).
http://localhost:5173andhttp://127.0.0.1:5173are different origins.
Real-Time Updates Not Arriving
- Proxy blocks WebSockets. Reverse proxies/ingresses must allow the WebSocket upgrade for Socket.IO connections.
- Multiple server instances without Redis. Updates only reach clients
connected to the same instance unless
REDIS_HOSTis configured on all instances - see Self-Hosting. - Subscribing too early.
Record.subscribeToQuery()throws if the client hasn't finished authenticating (actorIdnot yet known). Awaitlr.ensureUserIdIsKnown()or checklr.isAuthenticated()first. The React hookuseKeyValueRecordshandles this for you.
Concurrent Edits Overwrite Each Other
Key-value records merge concurrent updates per field (CRDT), but array fields are replaced wholesale - last write wins for the entire array, with no element-level merging. If concurrent additions to a list must not drop entries, model list items as separate records or facts instead of an array field. See Key-Value Records.
403 With "Not Enough Storage Space Available"
The accountable user or organization has exceeded its storage quota. Check
usage with lr.getQuota(nodeId) and see Quotas and
Accountability - by default storage is charged to the
record's accountee, which you can change with an $isAccountableFor fact.
Inspecting the Server State
-
Look at the facts table. All authorization state is plain data in PostgreSQL:
-
Server logs. The server logs structured JSON to stdout, including a line whenever a request was rejected because of unauthorized facts.
-
Liveness.
GET /oidc/discoveryanswers without authentication - if it doesn't respond, the server isn't the problem layer to debug first.