Locara

43 — Registry API Contract

The HTTP API surface between the locara CLI, the website, the registry backend, and (eventually) the optional Locara Manager utility. This is the contract that lets multiple components be built independently against a shared interface.

This is v1 of the API. Breaking changes require an ADR + version bump (/v2/...).

Base URL

  • Production: https://registry.locara.app/v1/
  • Staging: https://registry-staging.locara.app/v1/
  • Local development: http://localhost:8787/v1/

All endpoints are HTTPS in production. Internal-network calls (CI → DB) skip TLS where appropriate.

Authentication

CallerAuth method
Public read (website, CLI lookups)None — anonymous
Publisher writes (locara publish, account management)GitHub OAuth → JWT bearer token
CI (build pipeline)GitHub Actions OIDC token verified by registry
Admin (trust group)GitHub OAuth + maintainer-list check

JWT tokens are short-lived (1 hour) and refreshed via standard OAuth refresh flow. Hardware-key-signed assertions optional for high-trust publishers.

Endpoints

Public read endpoints (no auth)

GET /apps

List apps in the catalog.

GET /v1/apps?category=productivity&fully_local=true&page=1&limit=50

Response:

{
  "apps": [
    {
      "id": "kingtongchoo/transcribe",
      "publisher": "kingtongchoo",
      "name": "transcribe",
      "latest_version": "0.1.0",
      "display_name": "Transcribe",
      "description": "Fully-local audio and meeting transcription.",
      "category": "productivity",
      "tags": ["audio", "transcription"],
      "icon_url": "https://cdn.locara.app/apps/kingtongchoo/transcribe/0.1.0/icon.png",
      "fully_local": true,
      "device_fit": { "min_ram_gb": 8 },
      "stats": {
        "installs": 142,
        "rating_avg": 4.7,
        "rating_count": 23
      },
      "verified_publisher": "domain"
    }
  ],
  "page": 1,
  "limit": 50,
  "total": 27
}

Filters: category, fully_local, device_fit_max_ram, verified_publisher, tag, q (full-text search).

GET /apps/{publisher}/{name}

Single app’s full record.

Response:

{
  "id": "kingtongchoo/transcribe",
  "publisher": "kingtongchoo",
  "name": "transcribe",
  "latest_version": "0.1.0",
  "versions": ["0.1.0"],
  "manifest": { /* full locara.json */ },
  "long_description_md": "...",
  "screenshots": ["https://cdn.locara.app/...", "..."],
  "homepage": "https://...",
  "repository": "https://github.com/...",
  "license": "Apache-2.0",
  "publisher_verified": "domain",
  "first_published_at": "2026-05-15T10:00:00Z",
  "stats": { ... }
}

GET /apps/{publisher}/{name}/{version}

Specific version’s record.

{
  "id": "kingtongchoo/transcribe@0.1.0",
  "manifest": { /* */ },
  "lockfile": { /* */ },
  "artifact": {
    "url": "https://cdn.locara.app/apps/kingtongchoo/transcribe/0.1.0.dmg",
    "sha256": "abc...",
    "size_bytes": 1234567890
  },
  "provenance": {
    "build_url": "https://github.com/...",
    "commit_sha": "def...",
    "sigstore_log_id": "ghi..."
  },
  "review": {
    "status": "approved",
    "auto": true,
    "reviewed_at": "2026-05-15T10:08:23Z"
  },
  "published_at": "2026-05-15T10:08:23Z"
}

GET /apps/{publisher}/{name}/latest

Convenience: latest version’s record (same shape as version-specific). Used by app updaters.

GET /revocations

List of revoked app versions (kill-switch). Apps poll this on launch.

{
  "revocations": [
    {
      "publisher": "baduser",
      "name": "evil-app",
      "version": "0.5.2",
      "reason": "malware",
      "revoked_at": "2026-06-01T14:20:00Z",
      "signature": "..."
    }
  ],
  "list_signature": "...",  // Locara-signed, allows clients to verify integrity
  "list_updated_at": "2026-06-01T14:20:00Z"
}

The list is paginated (/revocations?since=<timestamp> for incremental sync). Apps cache the full list locally + sync deltas.

GET /models

List of curated model manifests.

GET /models/{model-id}

Single curated model manifest (with HF artifact URL + recommended params).

GET /tools and GET /tools/{tool-id}

Curated wasm tool registry.

GET /components and GET /components/{component-id}

Curated component registry (shadcn-style).

Publisher endpoints (require auth)

POST /auth/oauth/start

Start GitHub OAuth flow. Redirects to GitHub.

POST /auth/oauth/callback

OAuth callback. Returns JWT.

GET /me

Current publisher account.

{
  "id": "kingtongchoo",
  "github_username": "kingtongchoo",
  "verified_domain": "kingtongchoo.dev",
  "publisher_signing_pubkey": "ed25519:...",
  "apps_published": 2,
  "two_fa_enabled": true,
  "created_at": "2026-04-01T00:00:00Z"
}

PATCH /me

Update account info (display name, contact email, etc.).

POST /me/verify-domain

Initiate DNS-TXT verification for the “domain-verified” badge.

// Request
{ "domain": "kingtongchoo.dev" }

// Response
{
  "verification_token": "locara-verify-abc123def456",
  "expected_dns_record": {
    "type": "TXT",
    "name": "_locara-verify.kingtongchoo.dev",
    "value": "abc123def456"
  },
  "expires_at": "2026-05-08T00:00:00Z"
}

Then POST /me/verify-domain/check to verify the DNS record exists.

POST /apps/submit

Initiate a new app submission. Triggers CI build.

// Request
{
  "repo_url": "https://github.com/kingtongchoo/transcribe-locara",
  "commit_sha": "abc123def456...",
  "manifest_signature": "...",   // Ed25519 signature of the manifest by publisher's signing key
  "expected_version": "0.1.0"     // matches the manifest's version field
}

// Response
{
  "submission_id": "sub_abc123",
  "status": "queued",
  "ci_url": "https://github.com/locara/ci-runs/...",
  "estimated_completion": "2026-05-01T10:15:00Z"
}

Server-side: validates the publisher owns the repo (or is granted permission), kicks off GitHub Actions, returns submission ID. CI updates submission status as it progresses.

GET /submissions/{submission_id}

Check submission status.

{
  "id": "sub_abc123",
  "publisher": "kingtongchoo",
  "app_id": "kingtongchoo/transcribe",
  "version": "0.1.0",
  "status": "building",
  "progress": [
    { "step": "verify", "status": "complete", "duration_ms": 4200 },
    { "step": "test", "status": "complete", "duration_ms": 38000 },
    { "step": "build", "status": "in_progress" },
    { "step": "sign", "status": "pending" },
    { "step": "notarize", "status": "pending" },
    { "step": "review", "status": "pending" }
  ],
  "ci_url": "https://github.com/locara/ci-runs/...",
  "error": null
}

GET /me/submissions

List the publisher’s submissions.

POST /apps/{publisher}/{name}/unpublish

Mark a version (or all versions) as unpublished. Doesn’t revoke (which is more aggressive); just removes from public catalog.

{
  "versions": ["0.1.0"],  // or "all"
  "reason": "discontinued"
}

The artifact remains in the registry for users who already installed; new installs from /apps/... return 410 Gone.

Reviews + ratings (require auth)

POST /apps/{publisher}/{name}/reviews

Leave a review.

{
  "version": "0.1.0",
  "rating": 5,
  "title": "Works exactly as advertised",
  "body": "..."
}

Server requires verifiable install (anti-spam): user must have installed the app in question (proven via signed install record from the runtime).

GET /apps/{publisher}/{name}/reviews

Public reviews list.

{
  "reviews": [
    {
      "id": "rev_abc",
      "rating": 5,
      "title": "...",
      "body": "...",
      "version_reviewed": "0.1.0",
      "author_username": "alice42",
      "author_verified_install": true,
      "created_at": "2026-06-01T10:00:00Z"
    }
  ]
}

POST /reviews/{review_id}/helpful

Vote a review as helpful. Auth-required to prevent spam.

Admin endpoints (trust group only)

POST /admin/revoke

Revoke a published app version. Requires trust-group auth + reason.

{
  "publisher": "baduser",
  "name": "evil-app",
  "versions": ["0.5.2"],     // or "all"
  "reason": "malware",
  "public_advisory_url": "https://locara.app/security/2026-06-01-baduser-evil-app",
  "signature": "..."           // multi-party signature for high-impact revocations
}

Effect: appears in /revocations immediately; affected apps refuse to launch on next start.

POST /admin/review/{submission_id}

Approve or reject a queued submission.

{
  "decision": "approved" | "rejected" | "needs-changes",
  "reviewer_notes": "...",
  "signature": "..."
}

GET /admin/submissions/queue

Submissions awaiting human review.

POST /admin/publishers/{publisher_id}/suspend

Suspend a publisher account (e.g., during compromise investigation).

POST /admin/audit-log/append

Internal admin actions are logged to an append-only audit trail.

CI endpoints (CI service auth only)

POST /ci/submission/{id}/status

CI updates submission status as it progresses through build / sign / notarize / attest.

POST /ci/submission/{id}/upload

CI uploads the signed artifact + provenance attestation.

POST /ci/submission/{id}/complete

CI declares the build complete; triggers review queue.

Data formats

Errors

All errors:

{
  "error": {
    "code": "validation_failed",
    "message": "Manifest schema is invalid",
    "details": [
      { "field": "version", "issue": "must be valid semver" }
    ],
    "request_id": "req_abc123"
  }
}

Error codes:

  • unauthenticated
  • unauthorized
  • validation_failed
  • not_found
  • conflict (e.g., version already exists)
  • rate_limited
  • submission_failed
  • internal_error

Pagination

Cursor-based for stability:

?cursor=eyJpZCI6...

Response includes next_cursor if more results exist.

Versioning

API version is in the URL path (/v1/...). Breaking changes go to /v2/.... Both versions supported in parallel for at least 12 months after /v2/ ships.

Rate limits

Anonymous: 60 req/min/IP. Authenticated: 600 req/min/account. CI: high (500 req/sec aggregate).

429 returned with Retry-After header.

Webhook subscriptions (publishers)

Publishers can subscribe to webhooks for events relevant to their apps.

POST /me/webhooks
{
  "url": "https://my-build-system/webhook",
  "events": ["submission.completed", "review.decided", "revocation.applied"],
  "signing_secret": "..."
}

Webhooks signed with HMAC-SHA256; receiver verifies.

OpenAPI specification

A formal OpenAPI 3.1 spec is generated and published at https://registry.locara.app/openapi.json. SDKs can be code-generated.

(open) Whether to publish openapi-generated clients (Rust, TS, Python, Go) or expect users to generate their own.

Implementation notes

  • The API runs as Cloudflare Workers + Postgres (Neon). Stateless workers; all state in DB.
  • All endpoints are idempotent where appropriate (POST endpoints can include Idempotency-Key header).
  • All write endpoints require CSRF protection (origin checks for browser callers; not needed for CLI).
  • Audit logging on all admin endpoints + publisher account changes.
  • Standard 60-day retention on logs (operational); permanent retention for security-relevant events.

Open questions

  • (open) GraphQL alternative? Probably not — REST is simpler for our use cases.
  • (open) WebSocket for live submission status streaming? Probably yes for CI dashboards; could be SSE.
  • (open) Federation with third-party registries — protocol for cross-registry discovery? Defer to v2.
  • (open) Rate limit tiers for verified publishers? Probably yes; verified gets higher limits.
  • (open) Public API for “list all apps that use model X” / “all apps with capability Y”? Useful for security research; should be public.

Cross-references