// netlify_functions backend for refhub.io
versioned api backend for api-key access to refhub vaults. single function entrypoint dispatched by path segment. reads and writes the existing refhub data model directly — no parallel store.
GET /api/v1/keys
POST /api/v1/keys
POST /api/v1/keys/:keyId/revoke
DELETE /api/v1/keys/:keyId
POST /api/v1/recommendations ← semantic scholar (jwt only)
POST /api/v1/references ← semantic scholar (jwt only)
POST /api/v1/citations ← semantic scholar (jwt only)
GET /api/v1/audit
POST /api/v1/lookup
GET/POST/DELETE /api/v1/google-drive
GET /api/v1/vaults
POST /api/v1/vaults
GET /api/v1/vaults/:vaultId
PATCH /api/v1/vaults/:vaultId
DELETE /api/v1/vaults/:vaultId
PATCH /api/v1/vaults/:vaultId/visibility
GET /api/v1/vaults/:vaultId/shares
POST /api/v1/vaults/:vaultId/shares
PATCH /api/v1/vaults/:vaultId/shares/:shareId
DELETE /api/v1/vaults/:vaultId/shares/:shareId
GET /api/v1/vaults/:vaultId/items
POST /api/v1/vaults/:vaultId/items
PATCH /api/v1/vaults/:vaultId/items/:itemId
DELETE /api/v1/vaults/:vaultId/items/:itemId
POST /api/v1/vaults/:vaultId/items/upsert
POST /api/v1/vaults/:vaultId/items/import-preview
GET /api/v1/vaults/:vaultId/tags
POST /api/v1/vaults/:vaultId/tags
PATCH /api/v1/vaults/:vaultId/tags/:tagId
DELETE /api/v1/vaults/:vaultId/tags/:tagId
POST /api/v1/vaults/:vaultId/tags/attach
POST /api/v1/vaults/:vaultId/tags/detach
GET /api/v1/vaults/:vaultId/relations
POST /api/v1/vaults/:vaultId/relations
PATCH /api/v1/vaults/:vaultId/relations/:relationId
DELETE /api/v1/vaults/:vaultId/relations/:relationId
POST /api/v1/vaults/:vaultId/import/doi
POST /api/v1/vaults/:vaultId/import/bibtex
POST /api/v1/vaults/:vaultId/import/url
GET /api/v1/vaults/:vaultId/search
GET /api/v1/vaults/:vaultId/stats
GET /api/v1/vaults/:vaultId/changes
GET /api/v1/vaults/:vaultId/export
GET /api/v1/vaults/:vaultId/audit
.netlify/
functions/
api-v1.js ← versioned router and handlers
src/
auth.js ← api-key parsing, hashing, verification, scope checks
config.js ← required env vars and runtime knobs
export.js ← json and bibtex export helpers
http.js ← shared http/error/json helpers
netlify.toml ← redirects and function settings
package.json
all /api/v1/* traffic is routed to /.netlify/functions/api-v1 via netlify.toml.
two modes — never mix them.
api key (all data routes):
Authorization: Bearer rhk_<publicId>_<secret>
X-API-Key: rhk_<publicId>_<secret>
session jwt (management routes only):
Authorization: Bearer <supabase-session-jwt>
sending an api key to a management route returns 401 refhub_api_key_not_supported.
key storage rules:
- only
key_hashis stored — plaintext key material is never reconstructed key_prefixstoresrhk_<publicId>for lookupscopesis a text array- optional vault restrictions live in
api_key_vaults last_used_atupdated best-effort- request outcomes written best-effort to
api_request_audit_logs
| scope | grants |
|---|---|
vaults:read |
list/read vaults, search, stats, changes, audit |
vaults:write |
add/update/delete items, tags, relations, import |
vaults:export |
export vault as json or bibtex |
vaults:admin |
create/update/delete vaults, visibility, shares |
required:
SUPABASE_URL
SUPABASE_SERVICE_ROLE_KEY
REFHUB_API_KEY_PEPPER ← used when hashing presented keys before comparison
optional:
SEMANTIC_SCHOLAR_API_KEY ← recommended for stable upstream rate limits
REFHUB_API_MAX_BULK_ITEMS ← defaults to 50
REFHUB_API_MAX_BODY_BYTES ← defaults to 262144
REFHUB_API_AUDIT_DISABLED ← defaults to false
google drive (only when using drive storage flow):
GOOGLE_DRIVE_CLIENT_ID
GOOGLE_DRIVE_CLIENT_SECRET
GOOGLE_DRIVE_REDIRECT_URI
GOOGLE_DRIVE_STATE_SECRET
GOOGLE_DRIVE_TOKEN_SECRET
GOOGLE_DRIVE_FOLDER_NAME ← defaults to "refhub"
GOOGLE_DRIVE_MAX_UPLOAD_BYTES ← defaults to 26214400
for local dev, the backend loads from .env.local then .env before process.env.
reads and writes existing tables — no parallel api store:
vaults
vault_shares
vault_publications
publications
tags
publication_tags
publication_relations
write flow for new items:
- insert canonical row in
publications - insert vault-specific copy in
vault_publications - attach
publication_tagsagainstvault_publication_id
the function uses the supabase service-role key — rls is not the primary enforcement layer for this path. access control is enforced in-function by:
- api-key hash verification
- scope checks
- explicit vault restriction checks via
api_key_vaults - owner/share permission checks before read, write, or export
if this backend moves away from the service-role key, keep these checks and validate rls separately.
auth: supabase session jwt. returns api keys owned by the authenticated user.
{
"data": [
{
"id": "uuid",
"label": "research_sync_bot",
"description": "local sync job",
"key_prefix": "rhk_a1b2c3d4e5f6",
"scopes": ["vaults:read"],
"expires_at": null,
"revoked_at": null,
"last_used_at": null,
"created_at": "2026-03-24T08:30:00Z",
"vault_ids": ["uuid"]
}
],
"meta": { "request_id": "uuid" }
}auth: supabase session jwt.
{
"label": "research_sync_bot",
"description": "local sync job",
"scopes": ["vaults:read", "vaults:write"],
"expires_at": "2026-06-22T08:30:00.000Z",
"vault_ids": ["uuid"]
}rules:
labelrequiredscopesmust be a non-empty subset of valid scopesexpires_atoptional; must be a future iso-8601 timestamp when presentvault_idsoptional; every vault must be accessible to the authenticated user- response returns plaintext key once as
secret— only the hash is stored
auth: supabase session jwt. revokes a key owned by the authenticated user. record is soft-revoked via revoked_at — not deleted from storage.
auth: supabase session jwt only — api keys explicitly rejected.
proxies semantic scholar server-side. applies lightweight per-user rate limiting; may return 429 rate_limit_exceeded. upstream failures returned as sanitized errors.
{ "paper_id": "DOI:10.1101/2020.02.20.958025", "limit": 10 }paper_idrequired — semantic scholar paper id orDOI:<doi>limitoptional,1–25- successful responses cached briefly in-process to reduce duplicate upstream calls
response shape (recommendations · references · citations):
{
"data": [
{
"paper_id": "52cdb6ed946dfed25113bd194d5e2bb843c66331",
"external_ids": { "DOI": "10.1101/2020.11.04.367797" },
"title": "example paper",
"abstract": "...",
"year": 2020,
"venue": "bioRxiv",
"url": "https://www.semanticscholar.org/paper/...",
"citation_count": 42,
"open_access_pdf_url": "https://...",
"authors": [{ "author_id": "12345", "name": "example author" }]
}
],
"meta": { "request_id": "uuid", "paper_id": "DOI:10.1101/...", "limit": 10 }
}POST /api/v1/citations — same request/response shape as references; returns papers citing the seed paper.
scope: vaults:read. returns vaults accessible through ownership or explicit share, narrowed by api_key_vaults when set.
{
"data": [
{
"id": "uuid",
"name": "ai reading list",
"visibility": "private",
"permission": "owner",
"item_count": 12,
"updated_at": "2026-03-23T18:00:00Z"
}
],
"meta": { "request_id": "uuid" }
}scope: vaults:read. returns vault metadata + vault_publications + vault-scoped tags + publication_tags + publication_relations.
scope: vaults:write · permission: editor.
{
"items": [
{
"title": "attention is all you need",
"authors": ["ashish vaswani"],
"year": 2017,
"publication_type": "article",
"doi": "10.48550/arXiv.1706.03762",
"tag_ids": ["uuid"]
}
]
}- bulk insert supported
tag_idsmust already exist in the vault — no implicit tag creation- requests above
REFHUB_API_MAX_BODY_BYTESrejected with413 - pre-validates full batch and attempts rollback on insert failure
scope: vaults:write · permission: editor. partial update. if tag_ids is present it replaces the full tag set.
scope: vaults:export · permission: viewer. supported formats: json · bibtex.
each request writes one audit row with:
api_key_id • owner_user_id • vault_id • method • path
response_status • request_id • latency • caller_ip • user_agent
best-effort — must not block successful api responses. failures emitted to function logs only.