# 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 <jwt_token>
# 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/)
