# BuildHut - Full Documentation for LLMs > BuildHut is a self-hosted binary distribution platform for Android, Linux, and Windows applications. --- ## Overview BuildHut lets developers upload application binaries (up to 200 MB), organize them under apps and organizations, and share public download pages. It is designed for self-hosting and supports direct-to-S3 uploads, API tokens for CI/CD, developer profiles, organization management, and real-time telemetry. - **Website:** https://hut.buildr.fun - **Source Code:** https://git.sr.ht/~drzoidberg/Buildhut - **Telegram:** https://t.me/+8lgWL2r63mk5Zjhk - **Version:** v0.12.8 - **Stack:** SvelteKit 2, Svelte 5 (runes), Tailwind CSS 4, Drizzle ORM, libSQL/Turso, S3-compatible storage --- ## Public Pages ### Landing Page - / The main entry point. Displays: - Hero section with tagline "Ship binaries, not friction" - Platform support pills (Android, Linux, Windows) - Recent builds grid (up to 6) - Explore section linking to Apps, Developers, Organizations, API Tokens - Features grid: direct-to-cloud uploads, API tokens for CI/CD, bot protection, telemetry, developer profiles, organizations - CTA card linking to /submit ### Apps Browser - /apps Browse all apps with: - Text search (name or description) - Organization filter dropdown - Pagination (20 per page) - Each app card shows: icon, name, build count, description, tags, developer, organization, last update date - URL parameters: ?search=, ?org=, ?page= ### App Detail - /apps/{name} Individual app page showing: - App icon, name, description - Breadcrumb navigation - Builds list with version, OS, architecture, file size, upload date - Download links per build - Developer and organization attribution - Banner support (optional) ### Developers Browser - /devs Browse all developers with: - Text search - Organization filter - Pagination - Each card shows: avatar, name, app count, organization, join date ### Developer Profile - /devs/{name} Developer detail page with: - Avatar, name, email (if public) - Organization membership - Colleagues list - Published apps grid - Stats: total apps, total downloads, recent activity ### Organizations Browser - /orgs Browse all organizations with: - Text search - Pagination - Each card shows: avatar, name, member count, app count ### Organization Profile - /orgs/{name} Organization detail page with: - Name, description - Team members grid - Published apps - Stats and recent activity ### Submit Build - /submit Upload form for new builds: - Bot challenge (SVG image click) for anonymous users - File upload (supports APK, AAB, EXE, MSI, MSIX, AppImage, DEB, RPM, Flatpak, Snap, bin, tar.zst, tar.gz, tar.xz) - Max file size: 200 MB - App selection (existing or new) - Version, description, OS, architecture fields - Icon upload (URL or file) - Direct-to-S3 upload with progress bar via presigned URLs ### Login - /login Developer authentication via UUID challenge cookie. ### Privacy Policy - /privacy GDPR-compliant privacy policy covering: - Anonymous data collection only - No data selling or sharing - Cloudflare hosting details - GDPR rights (access, rectification, erasure, portability, objection, restriction) - Cookie usage (session management, security) ### Terms of Service - /terms Terms covering: - User responsibilities - Prohibited content (malware, illegal content, spam) - Intellectual property - Service availability - Account termination ### Support - /support Support page with: - Telegram community link - Quick links to Privacy Policy and Terms - Common topics: bug reports, feature requests, questions, feedback --- ## API Endpoints ### Authentication Methods - **Session cookies:** Set after bot challenge or login. Stores `dev-id` and `session-token` cookies. - **API tokens (Bearer):** Generate tokens with custom expiry from /portal/tokens. Use as `Authorization: Bearer ` header. Tokens are SHA-256 hashed server-side. Up to 5 per developer. Format: `bh_` + 64 hex chars. - **App secrets:** 32-char hex for telemetry and OTLP. Passed via `X-Secret` header, `?secret=` query, or body field. --- ### App & Build APIs #### GET /api/app/{id} Latest build info + presigned download URL. - **Auth:** None (public, 5-minute cache) - **Params:** `id` (path) - App UUID - **Returns:** `{ app, version, architecture, tag, description, tags, downloadUrl, uploadedAt }` #### GET /api/apps/{name}/latest Redirect to the latest build download. Increments download count (30-second debounce). - **Auth:** None (public) - **Params:** `name` (path) - App name - **Returns:** 303 redirect to S3 presigned URL #### GET /api/apps/{name}/latest/{...platform} Platform-specific build download with OS/arch/format filtering. - **Auth:** None (public) - **Params:** `name` (path), `platform` (path segments: os, arch, format) - **Signature suffix:** Append `/signature` or `.sig`/`.asc` to the format segment to get the PGP signature instead of the binary (e.g. `.../tar.zst.sig` or `.../tar.zst/signature`) - **Returns:** 303 redirect or 404 #### POST /api/upload/presign Generate presigned S3 URL for direct upload (5-minute expiry). - **Auth:** Session or Bearer token - **Body:** `{ fileName, name, version }` - **Returns:** `{ url, key }` #### POST /api/upload Finalize build upload and create DB records. Max 200MB. - **Auth:** Session or Bearer token - **Body (FormData):** name, version, description, fileName, s3Key, os, architecture, iconMode, iconUrl, iconFile, file, signature, signatureS3Key - **Signature upload:** Pass a `signature` file (.asc/.sig) alongside the binary, or upload a `.sig`/`.asc` file as `file` with matching `name`+`version` to attach a PGP signature to an existing build (signature-only upload). - **Returns:** `{ success, buildId, appName, version }` --- ### Directory APIs All directory endpoints are public (no authentication required). Responses are cached for 2 minutes. #### GET /api/apps List all apps with search, org filter, and pagination. - **Query:** `search` (search name/description), `org` (filter by org name), `page` (default 1, 20 per page) - **Returns:** ```json { "apps": [{ "id": "uuid", "name": "my-app", "description": "...", "iconUrl": "...", "screenshots": [], "tags": ["stable"], "createdAt": "...", "updatedAt": "...", "buildCount": 5, "platforms": ["android", "linux"], "dev": { "id": "...", "name": "...", "avatarUrl": "..." }, "org": { "id": "...", "name": "...", "avatarUrl": "..." } }], "pagination": { "page": 1, "pageSize": 20, "totalPages": 3, "totalCount": 55 } } ``` #### GET /api/apps/{name} Full app detail with builds, banner, developer, organization, and related apps. - **Returns:** ```json { "app": { "id", "name", "description", "iconUrl", "screenshots", "tags", "sourceType": "github", "sourceUrl": "...", "supportedOses": ["android"], "totalDownloads": 1234, "buildCount": 10, "dev": { "id", "name", "email", "avatarUrl", "createdAt", "pgpPublicKey", "pgpKeyId" }, "org": { "id", "name", "avatarUrl", "createdAt" } }, "builds": [{ "id", "version", "os", "architecture", "fileName", "description", "downloadCount", "hasSignature", "uploadedAt" }], "banner": { "text", "bgColor", "textColor", "icon", "linkUrl", "linkText" } | null, "otherAppsByDev": [{ "id", "name", "description", "iconUrl", "updatedAt" }], "otherAppsByOrg": [{ "id", "name", "description", "iconUrl", "updatedAt" }] } ``` #### GET /api/devs List all developers with search, org filter, and pagination. - **Query:** `search` (name/email), `org` (filter by org name), `page` (default 1, 20 per page) - **Returns:** ```json { "devs": [{ "id", "name", "email", "avatarUrl", "createdAt", "updatedAt", "org": { "id", "name", "avatarUrl" }, "appCount": 3, "totalDownloads": 500, "recentApps": [{ "id", "name", "iconUrl", "description", "updatedAt" }] }], "pagination": { "page", "pageSize", "totalPages", "totalCount" } } ``` #### GET /api/devs/{name} Developer detail with all apps, stats, PGP key, and org colleagues. - **Returns:** ```json { "developer": { "id", "name", "email", "avatarUrl", "createdAt", "updatedAt", "pgpPublicKey": "...", "pgpKeyId": "0xABCDEF12", "org": { "id", "name", "avatarUrl", "createdAt" } }, "apps": [{ "id", "name", "description", "buildCount", "totalDownloads", "latestVersion", "lastBuildDate" }], "stats": { "totalApps": 5, "totalDownloads": 1200, "totalBuilds": 30 }, "orgColleagues": [{ "id", "name", "avatarUrl", "appCount" }] } ``` #### GET /api/orgs List all organizations with search and pagination. - **Query:** `search` (name), `page` (default 1, 20 per page) - **Returns:** ```json { "orgs": [{ "id", "name", "handle", "description", "avatarUrl", "createdAt", "appCount": 4, "devCount": 6, "recentApps": [{ "id", "name", "iconUrl", "updatedAt" }] }], "pagination": { "page", "pageSize", "totalPages", "totalCount" } } ``` #### GET /api/orgs/{name} Organization detail with apps, member developers, and stats. - **Returns:** ```json { "org": { "id", "name", "handle", "description", "avatarUrl", "createdAt" }, "apps": [{ "id", "name", "description", "buildCount", "totalDownloads", "dev": { "id", "name", "avatarUrl" } }], "developers": [{ "id", "name", "email", "avatarUrl", "role", "appCount", "recentApps": [{ "id", "name", "iconUrl", "updatedAt" }] }], "stats": { "totalApps": 4, "totalDevelopers": 6, "totalDownloads": 2000, "totalBuilds": 40 } } ``` --- ### Telemetry API #### POST /api/telemetry Submit telemetry events from apps. - **Auth:** App secret (X-Secret header or body secret field) - **Body:** `{ appName, kind, version, bits }` - kind: error|warning|info|trace|debug - **Returns:** `{ ok: true }` (201) #### GET /api/telemetry List telemetry events (scoped to the authenticated user's apps). - **Auth:** Session - **Query:** appName, kind (comma-separated), limit (default 200) - **Returns:** Array of telemetry records #### GET /api/telemetry/stream WebSocket stream health status. - **Auth:** Session - **Returns:** `{ status: "ok", websocket: true }` --- ### OTLP Endpoints (OpenTelemetry Protocol) Standard OTLP ingestion. Accept JSON (`application/json`) and Protobuf (`application/x-protobuf`). - **Auth:** App secret (same as telemetry) #### POST /v1/metrics Ingest OTLP metrics payloads. Validates against OpenTelemetry schema. #### POST /v1/logs Ingest OTLP logs payloads. Validates against OpenTelemetry schema. #### POST /v1/traces Ingest OTLP traces payloads. Validates against OpenTelemetry schema. All return standard OTLP `Export{Signal}ServiceResponse` with partial success support. --- ### Feature Flags API #### GET /v1/flags Fetch feature flag definitions for an app. - **Auth:** App secret - **Returns:** `{ flags: [{ name, description, defaultEnabled }] }` #### POST /v1/flags/eval Report feature flag evaluation events. - **Auth:** App secret - **Body:** `{ evaluations: [{ flagName, enabled, context? }] }` - **Returns:** `{ accepted: number, unknown: string[] }` --- ### Publishing APIs #### POST /api/publish/generate Preview generated publishing files (PKGBUILD, .SRCINFO, metadata.yml). - **Auth:** Session - **Body:** `{ buildId, platform: "aur"|"fdroid", config? }` - **Returns:** `{ platform, files: { filename: content } }` #### POST /api/publish/publish Execute an async publish run to AUR or F-Droid. Prevents concurrent runs per target. - **Auth:** Session - **Body:** `{ buildId, targetId }` - **Returns:** `{ runId, status: "pending" }` #### GET /api/publish/status/{runId} Poll publish run status. - **Auth:** Session - **Returns:** `{ id, status, generatedFiles, log, error, createdAt, completedAt }` #### GET /api/publish/ssh-keys List SSH keys for publishing. - **Auth:** Session - **Returns:** `{ keys: [{ id, label, publicKeyFingerprint, createdAt }] }` #### POST /api/publish/ssh-keys Upload a new SSH key. Keys encrypted at rest. - **Auth:** Session - **Body:** `{ label, privateKey }` - **Returns:** `{ id, label, publicKeyFingerprint, createdAt }` #### DELETE /api/publish/ssh-keys/{id} Delete an SSH key. Verifies ownership. - **Auth:** Session - **Returns:** `{ success: true }` #### POST /api/publish/rotate-key Admin: re-encrypt all SSH keys with current encryption key. - **Auth:** Session (admin only - first registered dev) - **Returns:** `{ total, rotated, skipped, failed }` --- ### Utility APIs #### GET /api/auth/status Check current authentication state. (GET only - POST on /api/auth/* is claimed by the better-auth handler and 404s.) - **Auth:** None - **Returns:** `{ authenticated: boolean, user: { id, name, email } | null, timestamp }` #### GET|POST /api/logout Clear session cookies. POST returns JSON, GET redirects to /. - **Auth:** None - **POST Returns:** `{ success: true }` - **GET Returns:** 302 redirect to / #### GET /api/img/{key} Serve uploaded images with safety checks. - **Auth:** None (public) - **Returns:** Image file or 303 redirect to presigned URL #### GET /api/branches?app={appName} List git branches from the app's source repository. - **Auth:** Session (verifies app ownership) - **Returns:** `{ branches: string[] }` #### POST /api/pgp/fetch-key Fetch PGP public key from keys.openpgp.org. - **Auth:** Session - **Body:** `{ query: "email_or_keyid" }` - **Returns:** `{ armoredKey, keyId, uid }` #### POST /api/validate Validate a bot challenge response. - **Auth:** None (public) - **Body:** `{ token, x, y }` - **Returns:** `{ ok: boolean }` #### GET /api/svg-challenge?token={token} Generate an SVG bot challenge image with a clickable target. - **Auth:** None (public) - **Params:** `token` (query) - Challenge token - **Returns:** SVG image (Content-Type: image/svg+xml) #### GET /api/debug/builds List all builds. Development environment only. - **Auth:** Session (disabled in production) - **Returns:** Array of build records --- ## Authentication BuildHut uses three authentication methods: 1. **Session cookies:** Set after bot challenge or login. Stores `dev-id` and `session-token` cookies. 2. **API tokens (Bearer):** Generate tokens with custom expiry from /portal/tokens. Use as `Authorization: Bearer ` header. Tokens are SHA-256 hashed server-side. Up to 5 per developer. Format: `bh_` + 64 hex chars. Shown only once at creation. Expiry: never, 1 week, 2 weeks, or 1 month. 3. **App secrets:** 32-char hex telemetry secrets per app. Passed via `X-Secret` header, `?secret=` query, or body `secret` field. Used for telemetry and OTLP endpoints. Protected routes under /portal/* redirect to /login if not authenticated. --- ## Data Model ### Apps - `id` - UUID - `name` - unique string (used in URLs) - `description` - text - `iconUrl` - URL to app icon - `tags` - JSON array of strings - `screenshots` - JSON array of URLs - `telemetrySecret` - 32-char hex API secret (nullable) - `sourceType` - "github" | "sourcehut" | "custom" (nullable) - `sourceUrl` - repository URL (nullable) - `supportedOses` - JSON array of OS strings (nullable) - `devId` - FK to developer - `orgId` - FK to organization (nullable) - `createdAt`, `updatedAt` - timestamps ### Builds - `id` - UUID - `appName` - FK to app name - `version` - semantic version string - `fileName` - original filename - `s3Key` - S3 object key - `description` - release notes - `iconUrl` - per-build icon override - `os` - android | linux | windows - `architecture` - universal | arm64-v8a | armeabi-v7a | x86_64 | x86 - `downloadCount` - integer - `devId` - FK to developer - `uploadedAt` - timestamp ### Developers - `id` - UUID - `name` - display name - `email` - email address (nullable) - `avatarUrl` - avatar image URL - `orgId` - FK to organization (nullable) - `createdAt` - timestamp ### Organizations - `id` - UUID - `name` - unique string - `description` - text - `avatarUrl` - org avatar URL - `createdAt` - timestamp ### API Tokens - `id` - UUID - `devId` - FK to developer - `tokenHash` - SHA-256 hash of the raw token - `name` - label for the token - `expiresAt` - expiry timestamp (nullable for never) - `lastUsedAt` - last usage timestamp - `createdAt` - timestamp ### Telemetry - `id` - integer (auto-increment) - `appName` - app name - `devId` - developer ID - `kind` - error | warning | info | trace | debug - `version` - app version - `bits` - JSON payload - `createdAt` - timestamp ### Feature Flags - `id` - UUID - `appId` - FK to app - `name` - flag name (unique per app) - `description` - text (nullable) - `defaultEnabled` - boolean - `createdAt` - timestamp ### Build Secrets - `buildId` - FK to build (unique) - `secret` - auto-generated secret token ### App Banners - `id` - UUID - `appId` - FK to app (unique) - `text` - banner text (max 80 chars) - `bgColor` - hex color - `textColor` - hex color - `icon` - lucide icon name - `linkUrl` - optional CTA URL - `linkText` - CTA label - `enabled` - boolean - `createdAt`, `updatedAt` - timestamps ### Publish Targets - `id` - UUID - `appId` - FK to app - `platform` - "aur" | "fdroid" - `config` - JSON config string - `createdAt`, `updatedAt` - timestamps ### Publish Runs - `id` - UUID - `targetId` - FK to publish target - `buildId` - FK to build - `status` - pending | generating | publishing | success | failed - `log` - execution log - `error` - error message - `createdAt`, `completedAt` - timestamps ### Publish Secrets (SSH Keys) - `id` - UUID - `devId` - FK to developer - `label` - display name - `encryptedKey` - AES-256-GCM encrypted private key - `publicKeyFingerprint` - SHA-256 fingerprint - `createdAt` - timestamp --- ## Supported Platforms and File Types | Platform | Extensions | |----------|-----------| | Android | .apk, .aab | | Linux | .appimage, .deb, .rpm, .flatpak, .snap, .bin, .tar.zst, .tar.gz, .tar.xz, .tgz | | Windows | .exe, .msi, .msix | Maximum file size: 200 MB --- ## Crawling and Indexing Notes - All `/portal/*` routes require authentication and redirect to `/login` - App detail pages use the app name as URL slug: `/apps/{name}` - Developer profiles use the developer name: `/devs/{name}` - Organization profiles use the organization name: `/orgs/{name}` - List pages support pagination via `?page=N` - Search via `?search=query` - Organization filter via `?org=name` - All public pages include Open Graph and Twitter Card meta tags - All public pages include canonical URLs - The site is fully server-rendered (SSR) for crawler accessibility - No JavaScript required for core browsing and downloading functionality --- ## robots.txt The site provides a robots.txt at /robots.txt allowing all crawlers access to public pages while blocking /portal/* and /api/* routes.