openapi: 3.0.3
info:
  title: BrewMark API
  version: 1.0.0
  description: |
    BrewMark is a personalized coffee brewing recipe platform. This API powers the BrewMark iOS app, web platform, and roaster embed widgets.

    ## Authentication

    Most read endpoints are public. Write endpoints require authentication via JWT session cookies.

    **Authentication methods:**
    - **Magic Link** — Passwordless email-based login. POST to `/api/auth/magic-link` with an email, then verify via the link sent.
    - **Password** — Traditional email/password via `/api/auth/login`.
    - **Sign in with Apple** — iOS app flow via `/api/auth/apple`.

    Authenticated requests use an `HttpOnly` session cookie (`bm_session`) set after login.

    ## Rate Limiting

    Endpoints are rate-limited per IP address. Auth endpoints: 3-5 req/min. Voting: 30 req/min. General: varies by endpoint. Rate-limited responses return `429 Too Many Requests` with a `Retry-After` header.

    ## Pagination

    List endpoints support cursor-based pagination:
    - `limit` — Number of items per page (varies by endpoint, typically max 50)
    - `cursor` — Opaque cursor from previous response for next page

    Some endpoints also support offset-based pagination with `page` parameter.

    ## Errors

    All errors return JSON with an `error` field:
    ```json
    { "error": "Description of what went wrong" }
    ```
  contact:
    name: BrewMark Team
    url: https://brewmark.io
  license:
    name: Proprietary

servers:
  - url: https://brewmark.io
    description: Production
  - url: http://localhost:3000
    description: Local development

tags:
  - name: Health
    description: System health checks
  - name: Auth
    description: Authentication and session management
  - name: Coffees
    description: Coffee profiles and catalog
  - name: Recipes
    description: Community recipes and search
  - name: Roasters
    description: Roaster profiles and discovery
  - name: Equipment
    description: Grinders, machines, filters, and brew methods
  - name: User
    description: User profile, equipment, and preferences
  - name: Brew Logs
    description: Brew logging and history
  - name: Collections
    description: User recipe collections
  - name: Following
    description: Roaster follow system and feed
  - name: Adaptation
    description: Recipe adaptation and recommendations
  - name: Embed
    description: Embeddable widget API for roaster websites
  - name: Roaster Portal
    description: Roaster management endpoints (requires roaster membership)
  - name: Tracking
    description: Analytics and page view tracking

paths:
  /api/health:
    get:
      tags: [Health]
      summary: Health check
      description: Returns system health with database latency and migration status.
      responses:
        '200':
          description: System healthy
          content:
            application/json:
              schema:
                type: object
                properties:
                  status:
                    type: string
                    example: healthy
                  timestamp:
                    type: string
                    format: date-time
                  uptime:
                    type: number
                    description: Uptime in seconds
                  checks:
                    type: object
                    properties:
                      database:
                        type: object
                        properties:
                          status:
                            type: string
                            enum: [healthy, unhealthy]
                          latencyMs:
                            type: number
                      migrations:
                        type: object
                        properties:
                          status:
                            type: string

  # --- Auth ---
  /api/auth/register:
    post:
      tags: [Auth]
      summary: Register new account
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [email, password]
              properties:
                email:
                  type: string
                  format: email
                password:
                  type: string
                  minLength: 8
      responses:
        '201':
          description: Account created, session cookie set
        '409':
          description: Email already registered
        '429':
          description: Rate limited (3/min)

  /api/auth/login:
    post:
      tags: [Auth]
      summary: Login with email and password
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [email, password]
              properties:
                email:
                  type: string
                  format: email
                password:
                  type: string
      responses:
        '200':
          description: Login successful, session cookie set
        '401':
          description: Invalid credentials
        '429':
          description: Rate limited (5/min)

  /api/auth/magic-link:
    post:
      tags: [Auth]
      summary: Send magic link email
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [email]
              properties:
                email:
                  type: string
                  format: email
      responses:
        '200':
          description: Magic link sent
        '429':
          description: Rate limited (5/min)

  /api/auth/magic-link/verify:
    get:
      tags: [Auth]
      summary: Verify magic link token
      parameters:
        - name: token
          in: query
          required: true
          schema:
            type: string
      responses:
        '302':
          description: Redirects to app with session cookie set
        '400':
          description: Invalid or expired token

  /api/auth/apple:
    post:
      tags: [Auth]
      summary: Sign in with Apple
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [identityToken]
              properties:
                identityToken:
                  type: string
                email:
                  type: string
                  format: email
      responses:
        '200':
          description: Login successful
        '429':
          description: Rate limited (5/min)

  /api/auth/me:
    get:
      tags: [Auth]
      summary: Get current user
      security:
        - cookieAuth: []
      responses:
        '200':
          description: Authenticated user profile
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/User'
        '401':
          description: Not authenticated

  /api/auth/logout:
    post:
      tags: [Auth]
      summary: Log out
      security:
        - cookieAuth: []
      responses:
        '200':
          description: Session cleared

  # --- Coffees ---
  /api/coffees:
    get:
      tags: [Coffees]
      summary: List coffees
      description: Search and filter coffee profiles with pagination.
      parameters:
        - name: q
          in: query
          schema:
            type: string
          description: Search query (name, roaster, origin)
        - name: roaster
          in: query
          schema:
            type: string
          description: Filter by roaster name
        - name: roastLevel
          in: query
          schema:
            type: string
            enum: [LIGHT, MEDIUM_LIGHT, MEDIUM, MEDIUM_DARK, DARK]
        - name: origin
          in: query
          schema:
            type: string
        - name: coffeeType
          in: query
          schema:
            type: string
            enum: [SINGLE_ORIGIN, BLEND, DECAF]
        - name: sort
          in: query
          schema:
            type: string
            enum: [alpha, recent, recipes, trending]
          description: Sort order
        - name: limit
          in: query
          schema:
            type: integer
            maximum: 50
            default: 20
        - name: cursor
          in: query
          schema:
            type: string
          description: Pagination cursor
      responses:
        '200':
          description: List of coffees
          content:
            application/json:
              schema:
                type: object
                properties:
                  coffees:
                    type: array
                    items:
                      $ref: '#/components/schemas/Coffee'
                  nextCursor:
                    type: string
                    nullable: true
    post:
      tags: [Coffees]
      summary: Create coffee
      security:
        - cookieAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [name, roasterName, roastLevel]
              properties:
                name:
                  type: string
                roasterName:
                  type: string
                roastLevel:
                  type: string
                  enum: [LIGHT, MEDIUM_LIGHT, MEDIUM, MEDIUM_DARK, DARK]
                origin:
                  type: string
                flavorProfile:
                  type: string
                description:
                  type: string
      responses:
        '201':
          description: Coffee created

  /api/coffees/{id}:
    get:
      tags: [Coffees]
      summary: Get coffee by ID
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: integer
      responses:
        '200':
          description: Coffee details
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Coffee'
        '404':
          description: Coffee not found

  /api/coffees/{id}/stats:
    get:
      tags: [Coffees]
      summary: Get coffee statistics
      description: Aggregated stats including top grinders, machines, vote counts, and grind/ratio ranges.
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: integer
      responses:
        '200':
          description: Coffee statistics

  /api/coffees/suggestions:
    get:
      tags: [Coffees]
      summary: Fuzzy search suggestions
      parameters:
        - name: name
          in: query
          schema:
            type: string
        - name: roaster
          in: query
          schema:
            type: string
      responses:
        '200':
          description: Search suggestions

  # --- Recipes ---
  /api/recipes:
    get:
      tags: [Recipes]
      summary: List community recipes
      parameters:
        - name: q
          in: query
          schema:
            type: string
        - name: grinderId
          in: query
          schema:
            type: integer
        - name: machineId
          in: query
          schema:
            type: integer
        - name: roastLevel
          in: query
          schema:
            type: string
        - name: limit
          in: query
          schema:
            type: integer
            default: 20
        - name: cursor
          in: query
          schema:
            type: string
      responses:
        '200':
          description: List of recipes

  /api/recipes/{id}:
    get:
      tags: [Recipes]
      summary: Get recipe by ID
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: integer
      responses:
        '200':
          description: Recipe details
        '404':
          description: Recipe not found

  /api/recipes/search:
    get:
      tags: [Recipes]
      summary: Advanced recipe search
      description: Search recipes with complex filtering and Wilson score ranking. Supports taste matching for authenticated users.
      parameters:
        - name: q
          in: query
          schema:
            type: string
        - name: brewMethod
          in: query
          schema:
            type: string
        - name: roaster
          in: query
          schema:
            type: string
        - name: roastLevel
          in: query
          schema:
            type: string
        - name: flavorProfile
          in: query
          schema:
            type: string
        - name: grindRange
          in: query
          schema:
            type: string
            enum: [fine, medium, coarse]
        - name: brewTimeRange
          in: query
          schema:
            type: string
            enum: [short, medium, long]
        - name: sort
          in: query
          schema:
            type: string
            enum: [highest_rated, most_brewed, recommended, newest]
        - name: limit
          in: query
          schema:
            type: integer
            default: 20
        - name: cursor
          in: query
          schema:
            type: string
      responses:
        '200':
          description: Search results with pagination

  /api/recipes/{id}/share:
    post:
      tags: [Recipes]
      summary: Generate share URL
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: integer
      responses:
        '200':
          description: Share URL generated
          content:
            application/json:
              schema:
                type: object
                properties:
                  shareUrl:
                    type: string
                  recipeId:
                    type: integer

  # --- Community Recipes ---
  /api/community-recipes:
    get:
      tags: [Recipes]
      summary: List community recipes
      parameters:
        - name: coffeeId
          in: query
          schema:
            type: integer
        - name: grinderId
          in: query
          schema:
            type: integer
        - name: machineId
          in: query
          schema:
            type: integer
        - name: createdByMe
          in: query
          schema:
            type: boolean
        - name: limit
          in: query
          schema:
            type: integer
        - name: cursor
          in: query
          schema:
            type: string
      responses:
        '200':
          description: Community recipes sorted by upvotes
    post:
      tags: [Recipes]
      summary: Create community recipe
      security:
        - cookieAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [coffeeProfileId, grinderId, machineId, grindSetting, ratio, doseGrams, waterGrams]
              properties:
                coffeeProfileId:
                  type: integer
                grinderId:
                  type: integer
                machineId:
                  type: integer
                filterId:
                  type: integer
                grindSetting:
                  type: number
                ratio:
                  type: number
                doseGrams:
                  type: number
                waterGrams:
                  type: number
                brewTimeSeconds:
                  type: integer
                waterTempF:
                  type: number
                notes:
                  type: string
      responses:
        '201':
          description: Recipe created

  /api/community-recipes/{id}/vote:
    get:
      tags: [Recipes]
      summary: Get vote status
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: integer
      responses:
        '200':
          description: Current user's vote on this recipe
    post:
      tags: [Recipes]
      summary: Vote on recipe
      security:
        - cookieAuth: []
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: integer
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [vote]
              properties:
                vote:
                  type: string
                  enum: [UP, DOWN]
      responses:
        '200':
          description: Vote recorded
        '429':
          description: Rate limited (30/min)

  # --- Roasters ---
  /api/roasters:
    get:
      tags: [Roasters]
      summary: List roasters
      parameters:
        - name: search
          in: query
          schema:
            type: string
        - name: limit
          in: query
          schema:
            type: integer
            default: 20
        - name: cursor
          in: query
          schema:
            type: string
        - name: page
          in: query
          schema:
            type: integer
      responses:
        '200':
          description: List of roasters with coffee counts

  /api/roasters/{slug}:
    get:
      tags: [Roasters]
      summary: Get roaster by slug
      parameters:
        - name: slug
          in: path
          required: true
          schema:
            type: string
      responses:
        '200':
          description: Roaster profile with recipe count
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Roaster'
        '404':
          description: Roaster not found

  /api/roasters/{slug}/coffees:
    get:
      tags: [Roasters]
      summary: List roaster's coffees
      parameters:
        - name: slug
          in: path
          required: true
          schema:
            type: string
      responses:
        '200':
          description: Coffees from this roaster

  /api/roasters/{slug}/recipes:
    get:
      tags: [Roasters]
      summary: List roaster's recipes
      parameters:
        - name: slug
          in: path
          required: true
          schema:
            type: string
      responses:
        '200':
          description: Recipes for this roaster's coffees

  /api/roasters/{slug}/follow:
    get:
      tags: [Following]
      summary: Check follow status
      parameters:
        - name: slug
          in: path
          required: true
          schema:
            type: string
      responses:
        '200':
          description: Follow status and count
          content:
            application/json:
              schema:
                type: object
                properties:
                  isFollowing:
                    type: boolean
                  followerCount:
                    type: integer
    post:
      tags: [Following]
      summary: Follow a roaster
      security:
        - cookieAuth: []
      parameters:
        - name: slug
          in: path
          required: true
          schema:
            type: string
      responses:
        '201':
          description: Now following
    delete:
      tags: [Following]
      summary: Unfollow a roaster
      security:
        - cookieAuth: []
      parameters:
        - name: slug
          in: path
          required: true
          schema:
            type: string
      responses:
        '200':
          description: Unfollowed

  # --- Equipment ---
  /api/grinders:
    get:
      tags: [Equipment]
      summary: List grinder models
      parameters:
        - name: brand
          in: query
          schema:
            type: string
      responses:
        '200':
          description: Grinder catalog grouped by brand
          content:
            application/json:
              schema:
                type: object
                properties:
                  grinders:
                    type: array
                    items:
                      $ref: '#/components/schemas/Grinder'
                  brands:
                    type: array
                    items:
                      type: string

  /api/machines:
    get:
      tags: [Equipment]
      summary: List machine models
      parameters:
        - name: brand
          in: query
          schema:
            type: string
        - name: brewMethod
          in: query
          schema:
            type: string
      responses:
        '200':
          description: Machine catalog grouped by brand

  /api/filters:
    get:
      tags: [Equipment]
      summary: List filter types
      responses:
        '200':
          description: Filter catalog

  /api/brew-methods:
    get:
      tags: [Equipment]
      summary: List brew methods
      responses:
        '200':
          description: Available brew methods
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items:
                      type: object
                      properties:
                        id:
                          type: string
                        name:
                          type: string

  # --- User ---
  /api/user/profile:
    get:
      tags: [User]
      summary: Get taste profile
      security:
        - cookieAuth: []
      responses:
        '200':
          description: User's taste preferences
    put:
      tags: [User]
      summary: Update taste profile
      security:
        - cookieAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                strengthPreference:
                  type: number
                acidityTolerance:
                  type: number
                roastPreference:
                  type: number
                bodyPreference:
                  type: number
                bitternessPreference:
                  type: number
      responses:
        '200':
          description: Profile updated

  /api/user/grinders:
    get:
      tags: [User]
      summary: List user's grinders
      security:
        - cookieAuth: []
      responses:
        '200':
          description: User's grinder equipment
    post:
      tags: [User]
      summary: Add grinder to equipment
      security:
        - cookieAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [grinderId]
              properties:
                grinderId:
                  type: integer
                isDefault:
                  type: boolean
                nickname:
                  type: string
      responses:
        '201':
          description: Grinder added
    delete:
      tags: [User]
      summary: Remove grinder
      security:
        - cookieAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [grinderId]
              properties:
                grinderId:
                  type: integer
      responses:
        '200':
          description: Grinder removed

  /api/user/machines:
    get:
      tags: [User]
      summary: List user's machines
      security:
        - cookieAuth: []
      responses:
        '200':
          description: User's brewing machines
    post:
      tags: [User]
      summary: Add machine to equipment
      security:
        - cookieAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [machineId]
              properties:
                machineId:
                  type: integer
                isDefault:
                  type: boolean
                nickname:
                  type: string
      responses:
        '201':
          description: Machine added
    delete:
      tags: [User]
      summary: Remove machine
      security:
        - cookieAuth: []
      responses:
        '200':
          description: Machine removed

  /api/user/saved-recipes:
    get:
      tags: [User]
      summary: List bookmarked recipes
      security:
        - cookieAuth: []
      parameters:
        - name: limit
          in: query
          schema:
            type: integer
        - name: page
          in: query
          schema:
            type: integer
      responses:
        '200':
          description: Saved recipes
    post:
      tags: [User]
      summary: Bookmark a recipe
      security:
        - cookieAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [communityRecipeId]
              properties:
                communityRecipeId:
                  type: integer
      responses:
        '201':
          description: Recipe bookmarked
    delete:
      tags: [User]
      summary: Remove bookmark
      security:
        - cookieAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [communityRecipeId]
              properties:
                communityRecipeId:
                  type: integer
      responses:
        '200':
          description: Bookmark removed

  /api/user/account:
    delete:
      tags: [User]
      summary: Delete account permanently
      description: Permanently deletes user account and all related data. Community recipes are preserved with anonymous attribution.
      security:
        - cookieAuth: []
      responses:
        '200':
          description: Account deleted

  # --- Brew Logs ---
  /api/brew-logs:
    get:
      tags: [Brew Logs]
      summary: List brew logs
      security:
        - cookieAuth: []
      parameters:
        - name: coffeeProfileId
          in: query
          schema:
            type: integer
        - name: rating
          in: query
          schema:
            type: integer
        - name: savedAsRecipe
          in: query
          schema:
            type: boolean
        - name: limit
          in: query
          schema:
            type: integer
        - name: cursor
          in: query
          schema:
            type: string
      responses:
        '200':
          description: User's brew logs
    post:
      tags: [Brew Logs]
      summary: Log a brew
      security:
        - cookieAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                coffeeProfileId:
                  type: integer
                grinderId:
                  type: integer
                machineId:
                  type: integer
                grindSetting:
                  type: number
                doseGrams:
                  type: number
                waterGrams:
                  type: number
                brewTimeSeconds:
                  type: integer
                waterTempF:
                  type: number
                notes:
                  type: string
                rating:
                  type: integer
                  minimum: 1
                  maximum: 5
      responses:
        '201':
          description: Brew logged

  /api/brew-logs/{id}:
    get:
      tags: [Brew Logs]
      summary: Get brew log
      security:
        - cookieAuth: []
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: integer
      responses:
        '200':
          description: Brew log details
        '404':
          description: Not found
    patch:
      tags: [Brew Logs]
      summary: Update brew log
      security:
        - cookieAuth: []
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: integer
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                notes:
                  type: string
                rating:
                  type: integer
                outcome:
                  type: string
      responses:
        '200':
          description: Brew log updated
    delete:
      tags: [Brew Logs]
      summary: Delete brew log
      security:
        - cookieAuth: []
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: integer
      responses:
        '200':
          description: Brew log deleted

  # --- Collections ---
  /api/collections:
    get:
      tags: [Collections]
      summary: List collections
      security:
        - cookieAuth: []
      responses:
        '200':
          description: User's recipe collections
    post:
      tags: [Collections]
      summary: Create collection
      security:
        - cookieAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [name]
              properties:
                name:
                  type: string
                description:
                  type: string
      responses:
        '201':
          description: Collection created

  # --- Following & Feed ---
  /api/following/feed:
    get:
      tags: [Following]
      summary: Get follow feed
      description: Latest recipes from roasters you follow.
      security:
        - cookieAuth: []
      parameters:
        - name: limit
          in: query
          schema:
            type: integer
            default: 20
            maximum: 50
        - name: cursor
          in: query
          schema:
            type: string
      responses:
        '200':
          description: Feed of recipes from followed roasters

  # --- Adaptation ---
  /api/get-recipe:
    post:
      tags: [Adaptation]
      summary: Get personalized recipe
      description: Returns a starting recipe and community variants for a coffee/equipment combination, personalized to the user's taste profile.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [coffeeId, grinderId, machineId, volumeGrams]
              properties:
                coffeeId:
                  type: integer
                grinderId:
                  type: integer
                machineId:
                  type: integer
                filterId:
                  type: integer
                volumeGrams:
                  type: number
      responses:
        '200':
          description: Adapted recipe with variants
          content:
            application/json:
              schema:
                type: object
                properties:
                  startingPoint:
                    type: object
                    description: Adapted recipe parameters
                  recommendation:
                    type: object
                  variants:
                    type: array
                    items:
                      type: object

  /api/recommendations:
    get:
      tags: [Adaptation]
      summary: Get personalized recommendations
      security:
        - cookieAuth: []
      parameters:
        - name: limit
          in: query
          schema:
            type: integer
            default: 5
            maximum: 20
      responses:
        '200':
          description: Personalized recipe recommendations with match scores

  /api/discover:
    get:
      tags: [Adaptation]
      summary: Discover recipes
      description: System recommendations and community recipes adapted to your equipment.
      parameters:
        - name: roast
          in: query
          schema:
            type: string
            enum: [LIGHT, MEDIUM, DARK]
        - name: grinderId
          in: query
          required: true
          schema:
            type: integer
        - name: machineId
          in: query
          schema:
            type: integer
        - name: coffeeId
          in: query
          schema:
            type: integer
        - name: brewStrength
          in: query
          schema:
            type: string
      responses:
        '200':
          description: Discovery results

  # --- Embed ---
  /api/embed/recipes/{roasterSlug}:
    get:
      tags: [Embed]
      summary: Get recipes for embed widget
      description: Returns roaster's recipes formatted for the embeddable widget. Includes CORS validation against roaster's allowed domains.
      parameters:
        - name: roasterSlug
          in: path
          required: true
          schema:
            type: string
        - name: method
          in: query
          schema:
            type: string
          description: Filter by brew method
        - name: limit
          in: query
          schema:
            type: integer
            default: 12
            maximum: 24
      responses:
        '200':
          description: Recipes and embed configuration
          content:
            application/json:
              schema:
                type: object
                properties:
                  roaster:
                    type: object
                    properties:
                      name:
                        type: string
                      slug:
                        type: string
                  recipes:
                    type: array
                    items:
                      type: object
                  embedConfig:
                    type: object
                    properties:
                      accentColor:
                        type: string
                      theme:
                        type: string
                      showPoweredBy:
                        type: boolean

  # --- Roaster Portal ---
  /api/roaster-portal/dashboard:
    get:
      tags: [Roaster Portal]
      summary: Get dashboard data
      description: Roaster dashboard with stats, onboarding progress, recipe performance, and coffee overview.
      security:
        - cookieAuth: []
      responses:
        '200':
          description: Dashboard data
        '401':
          description: Not authenticated
        '403':
          description: Not a roaster member

  /api/roaster-portal/profile:
    get:
      tags: [Roaster Portal]
      summary: Get roaster profile
      security:
        - cookieAuth: []
      responses:
        '200':
          description: Roaster profile data
    put:
      tags: [Roaster Portal]
      summary: Update roaster profile
      security:
        - cookieAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                name:
                  type: string
                description:
                  type: string
                tagline:
                  type: string
                city:
                  type: string
                state:
                  type: string
                country:
                  type: string
                websiteUrl:
                  type: string
                instagramHandle:
                  type: string
                contactEmail:
                  type: string
                  format: email
                logoUrl:
                  type: string
                coverImageUrl:
                  type: string
      responses:
        '200':
          description: Profile updated

  /api/roaster-portal/coffees:
    get:
      tags: [Roaster Portal]
      summary: List roaster's coffees
      security:
        - cookieAuth: []
      responses:
        '200':
          description: Coffees with recipe, library, and brew counts
    post:
      tags: [Roaster Portal]
      summary: Create coffee
      security:
        - cookieAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [name, roastLevel]
              properties:
                name:
                  type: string
                slug:
                  type: string
                roastLevel:
                  type: string
                origin:
                  type: string
                flavorNotes:
                  type: string
                description:
                  type: string
      responses:
        '201':
          description: Coffee created

  /api/roaster-portal/analytics:
    get:
      tags: [Roaster Portal]
      summary: Get roaster analytics
      security:
        - cookieAuth: []
      responses:
        '200':
          description: Page views, embed clicks, and traffic data

  # --- Tracking ---
  /api/track:
    post:
      tags: [Tracking]
      summary: Track page view
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [roasterId]
              properties:
                roasterId:
                  type: integer
                coffeeId:
                  type: integer
      responses:
        '200':
          description: View tracked

  # --- Contact & Subscribe ---
  /api/contact:
    post:
      tags: [Health]
      summary: Submit contact form
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [name, email, subject, message]
              properties:
                name:
                  type: string
                email:
                  type: string
                  format: email
                subject:
                  type: string
                message:
                  type: string
      responses:
        '200':
          description: Message sent

  /api/subscribe:
    post:
      tags: [Health]
      summary: Subscribe to newsletter
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [email]
              properties:
                email:
                  type: string
                  format: email
                source:
                  type: string
      responses:
        '200':
          description: Subscribed
        '429':
          description: Rate limited (3/min)

  /api/roaster-claims:
    post:
      tags: [Roasters]
      summary: Submit roaster claim
      description: Claim ownership of an existing roaster profile or create a new one. Sends verification email to business address.
      security:
        - cookieAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [businessEmail]
              properties:
                roasterSlug:
                  type: string
                  description: Claim existing roaster by slug
                roasterName:
                  type: string
                  description: Create new roaster with this name
                businessEmail:
                  type: string
                  format: email
                proofUrl:
                  type: string
                  format: uri
                notes:
                  type: string
      responses:
        '200':
          description: Claim submitted, verification email sent
        '409':
          description: Pending claim exists or already a member

components:
  securitySchemes:
    cookieAuth:
      type: apiKey
      in: cookie
      name: bm_session
      description: JWT session cookie set after authentication

  schemas:
    User:
      type: object
      properties:
        id:
          type: integer
        email:
          type: string
          format: email
        profile:
          type: object
          properties:
            displayName:
              type: string
            strengthPreference:
              type: number
            acidityTolerance:
              type: number
            roastPreference:
              type: number

    Coffee:
      type: object
      properties:
        id:
          type: integer
        name:
          type: string
        slug:
          type: string
        roastLevel:
          type: string
          enum: [LIGHT, MEDIUM_LIGHT, MEDIUM, MEDIUM_DARK, DARK]
        origin:
          type: string
        flavorProfile:
          type: string
        description:
          type: string
        roaster:
          $ref: '#/components/schemas/Roaster'

    Roaster:
      type: object
      properties:
        id:
          type: integer
        name:
          type: string
        slug:
          type: string
        description:
          type: string
        tagline:
          type: string
        city:
          type: string
        state:
          type: string
        country:
          type: string
        logoUrl:
          type: string
        coverImageUrl:
          type: string
        websiteUrl:
          type: string
        isVerified:
          type: boolean

    Grinder:
      type: object
      properties:
        id:
          type: integer
        brand:
          type: string
        name:
          type: string
        minGrindSetting:
          type: number
        maxGrindSetting:
          type: number
        burrType:
          type: string
        burrSizeMm:
          type: number
