# Milestone 6: App Store Backend API **Status**: Decided **Goal**: REST API for app submission, review, and distribution. ## Decision **Go + Chi router** with JSON REST API: ``` Framework: Chi (lightweight, idiomatic) Validation: go-playground/validator/v10 OpenAPI: ogen (generated from spec) Middleware: Custom auth, rate limiting, logging Database: SQLite via repository pattern ``` ### Architecture ``` ┌─────────────────────────────────────────────────────────────────┐ │ mosis-portal container │ │ ┌────────────────────────────────────────────────────────────┐ │ │ │ Chi Router │ │ │ │ /v1/auth/* /v1/apps/* /v1/store/* /v1/keys/* │ │ │ └──────────────────────┬─────────────────────────────────────┘ │ │ │ │ │ ┌──────────┬───────────┼───────────┬──────────┐ │ │ │ Handlers │ Services │ Repos │ SQLite │ │ │ └──────────┴───────────┴───────────┴──────────┘ │ │ │ │ /volume1/mosis/data/portal.db │ │ /volume1/mosis/packages/ │ └─────────────────────────────────────────────────────────────────┘ ``` --- ## Overview The backend API serves the developer portal, CLI tools, and device-side app management. It handles authentication, app lifecycle, file storage, and telemetry ingestion. --- ## API Design Principles 1. **RESTful** - Standard HTTP methods and status codes 2. **JSON** - Request and response bodies in JSON 3. **Versioned** - `/v1/` prefix for breaking changes 4. **Consistent** - Same patterns across all endpoints 5. **Documented** - OpenAPI specification --- ## Base URL ``` Production: https://api.mosis.dev/v1 Staging: https://api.staging.mosis.dev/v1 Local: http://localhost:8080/v1 ``` --- ## Authentication ### Headers ``` Authorization: Bearer # or X-API-Key: mk_live_xxxxxxxx ``` ### Scopes | Scope | Description | |-------|-------------| | `apps:read` | Read app metadata | | `apps:write` | Create/update apps | | `versions:upload` | Upload new versions | | `versions:publish` | Publish versions | | `telemetry:read` | Read analytics | | `keys:manage` | Manage API keys | --- ## Endpoints ### Authentication ```yaml POST /v1/auth/oauth/github: summary: Start GitHub OAuth flow response: { redirect_url: string } GET /v1/auth/oauth/github/callback: summary: GitHub OAuth callback query: code: string state: string response: { access_token, refresh_token, user } POST /v1/auth/oauth/google: summary: Start Google OAuth flow response: { redirect_url: string } GET /v1/auth/oauth/google/callback: summary: Google OAuth callback POST /v1/auth/refresh: summary: Refresh access token body: { refresh_token: string } response: { access_token, refresh_token } POST /v1/auth/logout: summary: Invalidate tokens auth: required GET /v1/auth/me: summary: Get current user auth: required response: Developer ``` ### Apps ```yaml GET /v1/apps: summary: List developer's apps auth: required query: status: draft | published | suspended page: number limit: number response: { apps: App[], total: number } POST /v1/apps: summary: Create new app auth: required body: package_id: string # com.developer.appname name: string description?: string category?: string response: App GET /v1/apps/:id: summary: Get app details auth: required response: App PATCH /v1/apps/:id: summary: Update app metadata auth: required body: name?: string description?: string category?: string tags?: string[] response: App DELETE /v1/apps/:id: summary: Delete app (if no published versions) auth: required response: { success: true } ``` ### App Versions ```yaml GET /v1/apps/:id/versions: summary: List app versions auth: required query: status: draft | review | approved | published | rejected page: number limit: number response: { versions: AppVersion[], total: number } POST /v1/apps/:id/versions: summary: Create new version (get upload URL) auth: required body: version_name: string # 1.0.0 version_code: number # 1 release_notes?: string response: version: AppVersion upload_url: string # Presigned S3 URL upload_expires: string # ISO timestamp PUT /v1/apps/:id/versions/:vid/upload-complete: summary: Mark upload as complete, trigger validation auth: required response: AppVersion GET /v1/apps/:id/versions/:vid: summary: Get version details auth: required response: AppVersion POST /v1/apps/:id/versions/:vid/submit: summary: Submit version for review auth: required response: AppVersion # status: review POST /v1/apps/:id/versions/:vid/publish: summary: Publish approved version auth: required response: AppVersion # status: published DELETE /v1/apps/:id/versions/:vid: summary: Delete draft version auth: required response: { success: true } ``` ### Public App Store ```yaml GET /v1/store/apps: summary: Browse/search published apps auth: none query: q: string # Search query category: string sort: popular | recent | name page: number limit: number response: { apps: PublicApp[], total: number } GET /v1/store/apps/:package_id: summary: Get app store listing auth: none response: PublicApp GET /v1/store/apps/:package_id/download: summary: Get download URL for latest version auth: none (or device token) response: download_url: string version: string size: number signature: string GET /v1/store/apps/:package_id/versions/:version_code/download: summary: Get download URL for specific version auth: none response: { download_url, version, size, signature } ``` ### API Keys ```yaml GET /v1/keys: summary: List API keys auth: required response: { keys: APIKey[] } POST /v1/keys: summary: Create API key auth: required body: name: string permissions: string[] expires_at?: string response: key: APIKey secret: string # Only shown once! DELETE /v1/keys/:id: summary: Revoke API key auth: required response: { success: true } ``` ### Signing Keys ```yaml GET /v1/signing-keys: summary: List signing keys auth: required response: { keys: SigningKey[] } POST /v1/signing-keys: summary: Register signing key auth: required body: name: string public_key: string # PEM format response: SigningKey DELETE /v1/signing-keys/:id: summary: Revoke signing key auth: required response: { success: true } ``` ### Telemetry ```yaml POST /v1/telemetry/events: summary: Submit telemetry events (batch) auth: device token or API key body: events: - app_id: string event_type: string event_data: object timestamp: string response: { received: number } POST /v1/telemetry/crash: summary: Submit crash report auth: device token or API key body: app_id: string app_version: string crash_type: string message: string stack_trace: string context: object timestamp: string response: { id: string } GET /v1/apps/:id/analytics: summary: Get app analytics auth: required query: start_date: string end_date: string metrics: downloads | active_users | crashes response: data: - date: string downloads: number active_users: number crashes: number GET /v1/apps/:id/crashes: summary: Get crash reports auth: required query: version?: string page: number limit: number response: { crashes: CrashReport[], total: number } ``` --- ## Data Models (Go) ### Developer ```go type Developer struct { ID string `json:"id"` Email string `json:"email"` Name string `json:"name"` AvatarURL *string `json:"avatar_url,omitempty"` Verified bool `json:"verified"` CreatedAt time.Time `json:"created_at"` UpdatedAt time.Time `json:"updated_at"` } ``` ### App ```go type AppStatus string const ( AppStatusDraft AppStatus = "draft" AppStatusPublished AppStatus = "published" AppStatusSuspended AppStatus = "suspended" ) type App struct { ID string `json:"id"` PackageID string `json:"package_id"` Name string `json:"name"` Description *string `json:"description,omitempty"` Category *string `json:"category,omitempty"` Tags []string `json:"tags"` Status AppStatus `json:"status"` IconURL *string `json:"icon_url,omitempty"` LatestVersion *AppVersion `json:"latest_version,omitempty"` CreatedAt time.Time `json:"created_at"` UpdatedAt time.Time `json:"updated_at"` } ``` ### AppVersion ```go type VersionStatus string const ( VersionStatusDraft VersionStatus = "draft" VersionStatusUploading VersionStatus = "uploading" VersionStatusValidating VersionStatus = "validating" VersionStatusReview VersionStatus = "review" VersionStatusApproved VersionStatus = "approved" VersionStatusPublished VersionStatus = "published" VersionStatusRejected VersionStatus = "rejected" ) type AppVersion struct { ID string `json:"id"` AppID string `json:"app_id"` VersionName string `json:"version_name"` VersionCode int `json:"version_code"` PackageURL *string `json:"package_url,omitempty"` PackageSize *int64 `json:"package_size,omitempty"` Signature *string `json:"signature,omitempty"` Permissions []string `json:"permissions"` MinMosisVersion *string `json:"min_mosis_version,omitempty"` ReleaseNotes *string `json:"release_notes,omitempty"` Status VersionStatus `json:"status"` ReviewNotes *string `json:"review_notes,omitempty"` PublishedAt *time.Time `json:"published_at,omitempty"` CreatedAt time.Time `json:"created_at"` } ``` ### PublicApp ```go type PublicApp struct { PackageID string `json:"package_id"` Name string `json:"name"` Description *string `json:"description,omitempty"` Category *string `json:"category,omitempty"` Tags []string `json:"tags"` IconURL *string `json:"icon_url,omitempty"` AuthorName string `json:"author_name"` LatestVersion string `json:"latest_version"` DownloadCount int64 `json:"download_count"` Rating *float64 `json:"rating,omitempty"` CreatedAt time.Time `json:"created_at"` UpdatedAt time.Time `json:"updated_at"` } ``` ### APIKey ```go type APIKey struct { ID string `json:"id"` Name string `json:"name"` KeyPrefix string `json:"key_prefix"` // "mk_live_abc..." Permissions []string `json:"permissions"` LastUsedAt *time.Time `json:"last_used_at,omitempty"` ExpiresAt *time.Time `json:"expires_at,omitempty"` CreatedAt time.Time `json:"created_at"` } ``` ### CrashReport ```go type CrashReport struct { ID string `json:"id"` AppID string `json:"app_id"` AppVersion string `json:"app_version"` CrashType string `json:"crash_type"` Message string `json:"message"` StackTrace string `json:"stack_trace"` Context map[string]interface{} `json:"context"` Occurrences int `json:"occurrences"` FirstSeen time.Time `json:"first_seen"` LastSeen time.Time `json:"last_seen"` } ``` --- ## Error Handling ### Error Response Format ```json { "error": { "code": "VALIDATION_ERROR", "message": "Invalid package_id format", "details": { "field": "package_id", "constraint": "Must match pattern: ^[a-z][a-z0-9]*(\\.[a-z][a-z0-9]*)+$" } } } ``` ### Error Codes | Code | HTTP Status | Description | |------|-------------|-------------| | `UNAUTHORIZED` | 401 | Missing or invalid auth | | `FORBIDDEN` | 403 | Insufficient permissions | | `NOT_FOUND` | 404 | Resource not found | | `VALIDATION_ERROR` | 400 | Invalid request body | | `CONFLICT` | 409 | Resource already exists | | `RATE_LIMITED` | 429 | Too many requests | | `INTERNAL_ERROR` | 500 | Server error | --- ## Rate Limiting ### Headers ``` X-RateLimit-Limit: 1000 X-RateLimit-Remaining: 999 X-RateLimit-Reset: 1704067200 ``` ### Limits | Endpoint Category | Limit | Window | |-------------------|-------|--------| | Auth | 10 | 1 minute | | Read | 1000 | 1 hour | | Write | 100 | 1 hour | | Upload | 10 | 1 hour | | Telemetry | 10000 | 1 hour | --- ## Pagination ### Request ``` GET /v1/apps?page=2&limit=20 ``` ### Response ```json { "apps": [...], "pagination": { "page": 2, "limit": 20, "total": 45, "total_pages": 3 } } ``` --- ## Webhooks (Future) ```yaml POST /v1/webhooks: summary: Register webhook body: url: string events: string[] # version.published, crash.new secret: string Webhook Payload: headers: X-Mosis-Signature: sha256=xxx body: event: string data: object timestamp: string ``` --- ## OpenAPI Specification Full OpenAPI 3.0 spec will be generated and hosted at: - `https://api.mosis.dev/v1/openapi.json` - `https://api.mosis.dev/v1/docs` (Swagger UI) --- ## Implementation Structure (Go) ``` cmd/ └── portal/ └── main.go # Entry point, wire dependencies internal/ ├── api/ │ ├── router.go # Chi router setup │ ├── middleware/ │ │ ├── auth.go # JWT/API key validation │ │ ├── ratelimit.go # Token bucket rate limiter │ │ ├── logging.go # Request/response logging │ │ └── recovery.go # Panic recovery │ └── handlers/ │ ├── auth.go # OAuth2 + token endpoints │ ├── apps.go # App CRUD │ ├── versions.go # Version upload/publish │ ├── store.go # Public store API │ ├── keys.go # API/signing keys │ └── telemetry.go # Event ingestion ├── service/ │ ├── app.go # Business logic │ ├── version.go │ ├── auth.go │ └── storage.go # File storage operations ├── repository/ │ ├── sqlite/ # SQLite implementations │ │ ├── app.go │ │ ├── version.go │ │ ├── developer.go │ │ └── migrations/ # SQL migrations │ └── interfaces.go # Repository interfaces └── domain/ ├── app.go # Domain types ├── version.go └── developer.go pkg/ ├── validator/ # Custom validation rules └── signing/ # Ed25519 operations ``` ### Chi Router Setup ```go func NewRouter( authHandler *handlers.AuthHandler, appHandler *handlers.AppHandler, storeHandler *handlers.StoreHandler, ) chi.Router { r := chi.NewRouter() // Global middleware r.Use(middleware.RequestID) r.Use(middleware.RealIP) r.Use(middleware.Logger) r.Use(middleware.Recoverer) r.Use(middleware.Timeout(30 * time.Second)) // API v1 r.Route("/v1", func(r chi.Router) { // Public routes r.Group(func(r chi.Router) { r.Post("/auth/oauth/github", authHandler.GitHubOAuth) r.Get("/auth/oauth/github/callback", authHandler.GitHubCallback) r.Post("/auth/refresh", authHandler.Refresh) r.Get("/store/apps", storeHandler.ListApps) r.Get("/store/apps/{packageID}", storeHandler.GetApp) }) // Protected routes r.Group(func(r chi.Router) { r.Use(middleware.RequireAuth) r.Get("/auth/me", authHandler.Me) r.Route("/apps", func(r chi.Router) { r.Get("/", appHandler.List) r.Post("/", appHandler.Create) r.Route("/{appID}", func(r chi.Router) { r.Get("/", appHandler.Get) r.Patch("/", appHandler.Update) r.Delete("/", appHandler.Delete) r.Route("/versions", func(r chi.Router) { r.Get("/", appHandler.ListVersions) r.Post("/", appHandler.CreateVersion) }) }) }) }) }) return r } ``` --- ## Deliverables - [ ] OpenAPI specification - [ ] Authentication middleware - [ ] Rate limiting middleware - [ ] Auth endpoints - [ ] Apps CRUD endpoints - [ ] Versions endpoints with upload flow - [ ] Store public endpoints - [ ] API keys management - [ ] Signing keys management - [ ] Telemetry ingestion - [ ] Error handling - [ ] Request validation - [ ] Integration tests --- ## Open Questions 1. ~~GraphQL alongside REST?~~ → REST only for simplicity 2. WebSocket for real-time review status? → Consider for v1.1 3. ~~Batch operations for bulk updates?~~ → Not needed for MVP 4. ~~API versioning strategy (URL vs header)?~~ → URL prefix (/v1/) --- ## References - [REST API Design Guidelines](https://github.com/microsoft/api-guidelines) - [OpenAPI 3.0 Specification](https://swagger.io/specification/) - [HTTP Status Codes](https://httpstatuses.com/)