openapi: 3.1.0 info: title: famapp API version: "1.0.0" description: | Household-scoped REST JSON API for famapp (fam.ginnoir.com). Authenticate with an Auth.js session cookie (browser) or a household API bearer token (external clients). Generate bearer tokens in Settings → API tokens. servers: - url: https://fam.ginnoir.com description: Production - url: http://localhost:3000 description: Local development security: - bearerAuth: [] - sessionCookie: [] components: securitySchemes: bearerAuth: type: http scheme: bearer description: Household API token from Settings → API tokens sessionCookie: type: apiKey in: cookie name: authjs.session-token description: Auth.js session cookie (browser login) schemas: Error: type: object required: [error] properties: error: type: string OkResponse: type: object required: [ok] properties: ok: type: boolean enum: [true] Calendar: type: object properties: id: { type: string, format: uuid } name: { type: string } color: { type: string, nullable: true } visibility: { type: string, enum: [private, household] } ownerId: { type: string, format: uuid } CalendarInput: type: object required: [name] properties: name: { type: string } color: { type: string, nullable: true } visibility: { type: string, enum: [private, household], default: household } CalendarEvent: type: object properties: id: { type: string, format: uuid } calendarId: { type: string, format: uuid } title: { type: string } startAt: { type: string, format: date-time } endAt: { type: string, format: date-time } allDay: { type: boolean } location: { type: string, nullable: true } notes: { type: string, nullable: true } EventInput: type: object required: [calendarId, title, startAt, endAt] properties: calendarId: { type: string, format: uuid } title: { type: string } startAt: { type: string, format: date-time } endAt: { type: string, format: date-time } allDay: { type: boolean, default: false } location: { type: string, nullable: true } notes: { type: string, nullable: true } remindMinutesBefore: { type: integer, nullable: true } ListSummary: type: object properties: id: { type: string, format: uuid } type: { type: string } name: { type: string } archived: { type: boolean } openCount: { type: integer } doneCount: { type: integer } createdAt: { type: string, format: date-time } ListInput: type: object required: [type, name] properties: type: { type: string } name: { type: string } ListItem: type: object properties: id: { type: string, format: uuid } listId: { type: string, format: uuid } text: { type: string } qty: { type: string, nullable: true } notes: { type: string, nullable: true } done: { type: boolean } dueAt: { type: string, format: date-time, nullable: true } assigneeId: { type: string, format: uuid, nullable: true } position: { type: integer } ItemInput: type: object required: [text] properties: text: { type: string } qty: { type: string, nullable: true } notes: { type: string, nullable: true } dueAt: { type: string, format: date-time, nullable: true } assigneeId: { type: string, format: uuid, nullable: true } metadata: { type: object, nullable: true } Note: type: object properties: id: { type: string, format: uuid } householdId: { type: string, format: uuid } authorId: { type: string, format: uuid } title: { type: string } body: { type: string } pinned: { type: boolean } remindAt: { type: string, format: date-time, nullable: true } createdAt: { type: string, format: date-time } updatedAt: { type: string, format: date-time } NoteInput: type: object required: [title] properties: title: { type: string } body: { type: string, default: "" } pinned: { type: boolean, default: false } remindAt: { type: string, format: date-time, nullable: true } JournalEntry: type: object properties: id: { type: string, format: uuid } householdId: { type: string, format: uuid } userId: { type: string, format: uuid } recordedAt: { type: string, format: date-time } title: { type: string, nullable: true } body: { type: string } moods: { type: array, items: { type: string } } stress: { type: integer, minimum: 1, maximum: 10, nullable: true } pillsTaken: { type: boolean, nullable: true } createdAt: { type: string, format: date-time } updatedAt: { type: string, format: date-time } JournalEntryInput: type: object required: [recordedAt] properties: recordedAt: { type: string, format: date-time } title: { type: string, nullable: true } body: { type: string, default: "" } moods: { type: array, items: { type: string }, default: [] } stress: { type: integer, minimum: 1, maximum: 10, nullable: true } pillsTaken: { type: boolean, nullable: true } GardenContainer: type: object properties: id: { type: string, format: uuid } householdId: { type: string, format: uuid } name: { type: string } type: { type: string } locationNotes: { type: string, nullable: true } coverImageUrl: { type: string, nullable: true } images: { type: array, items: { type: string } } plantCount: { type: integer } createdAt: { type: string, format: date-time } updatedAt: { type: string, format: date-time } GardenContainerDetail: allOf: - $ref: "#/components/schemas/GardenContainer" - type: object properties: plants: type: array items: $ref: "#/components/schemas/PlantSummary" ContainerInput: type: object required: [name] properties: name: { type: string } type: { type: string, default: other } locationNotes: { type: string, nullable: true } coverImageUrl: { type: string, nullable: true } PlantSummary: type: object properties: id: { type: string, format: uuid } name: { type: string } scientificName: { type: string, nullable: true } healthStatus: { type: string } primaryImageUrl: { type: string, nullable: true } category: { type: string } PlantListItem: type: object properties: id: { type: string, format: uuid } containerId: { type: string, format: uuid, nullable: true } containerName: { type: string, nullable: true } name: { type: string } healthStatus: { type: string } primaryImageUrl: { type: string, nullable: true } category: { type: string } lastWateredAt: { type: string, format: date-time, nullable: true } hasOverdueCare: { type: boolean } PlantDetail: type: object properties: id: { type: string, format: uuid } householdId: { type: string, format: uuid } containerId: { type: string, format: uuid, nullable: true } containerName: { type: string, nullable: true } name: { type: string } scientificName: { type: string, nullable: true } speciesId: { type: string, nullable: true } category: { type: string } notes: { type: string, nullable: true } acquisitionDate: { type: string, nullable: true } growthStage: { type: string, nullable: true } healthStatus: { type: string } sunlight: { type: string, nullable: true } wateringNotes: { type: string, nullable: true } fertilizingNotes: { type: string, nullable: true } primaryImageUrl: { type: string, nullable: true } images: { type: array, items: { type: string } } createdAt: { type: string, format: date-time } updatedAt: { type: string, format: date-time } PlantInput: type: object required: [name] properties: name: { type: string } category: { type: string, default: other } containerId: { type: string, format: uuid, nullable: true } healthStatus: { type: string, default: healthy } growthStage: { type: string, nullable: true } scientificName: { type: string, nullable: true } speciesId: { type: string, nullable: true } sunlight: { type: string, nullable: true } wateringNotes: { type: string, nullable: true } fertilizingNotes: { type: string, nullable: true } notes: { type: string, nullable: true } acquisitionDate: { type: string, nullable: true } images: { type: array, items: { type: string }, default: [] } primaryImageUrl: { type: string, nullable: true } BangStats: type: object properties: total: { type: integer } recent: type: array items: $ref: "#/components/schemas/BangSummary" BangSummary: type: object properties: id: { type: string, format: uuid } occurredOn: { type: string, format: date, description: YYYY-MM-DD } recordedByName: { type: string, nullable: true } Bang: type: object properties: id: { type: string, format: uuid } occurredOn: { type: string, format: date } recordedBy: { type: string, format: uuid, nullable: true } createdAt: { type: string, format: date-time } BangInput: type: object properties: occurredOn: type: string format: date description: YYYY-MM-DD; defaults to today BangUpdateInput: type: object required: [occurredOn] properties: occurredOn: { type: string, format: date } paths: /api/v1/calendars: get: summary: List visible calendars tags: [Calendars] responses: "200": description: Calendar list content: application/json: schema: type: array items: { $ref: "#/components/schemas/Calendar" } "401": { description: Unauthorized, content: { application/json: { schema: { $ref: "#/components/schemas/Error" } } }, } post: summary: Create calendar tags: [Calendars] requestBody: required: true content: application/json: schema: { $ref: "#/components/schemas/CalendarInput" } responses: "201": description: Created calendar content: application/json: schema: { $ref: "#/components/schemas/Calendar" } "401": { description: Unauthorized } /api/v1/calendars/{id}: parameters: - name: id in: path required: true schema: { type: string, format: uuid } get: summary: Get calendar tags: [Calendars] responses: "200": content: application/json: schema: { $ref: "#/components/schemas/Calendar" } "404": { description: Not found } patch: summary: Update calendar tags: [Calendars] requestBody: content: application/json: schema: type: object properties: name: { type: string } visibility: { type: string, enum: [private, household] } color: { type: string, nullable: true } responses: "200": content: application/json: schema: { $ref: "#/components/schemas/Calendar" } delete: summary: Delete calendar tags: [Calendars] responses: "200": content: application/json: schema: { $ref: "#/components/schemas/OkResponse" } /api/v1/events: get: summary: List events in date range tags: [Events] parameters: - name: from in: query required: true schema: { type: string, format: date-time } - name: to in: query required: true schema: { type: string, format: date-time } - name: calendarIds in: query description: "all or comma-separated UUIDs" schema: { type: string, default: all } responses: "200": content: application/json: schema: type: array items: { $ref: "#/components/schemas/CalendarEvent" } "400": { description: Missing from/to } post: summary: Create event tags: [Events] requestBody: required: true content: application/json: schema: { $ref: "#/components/schemas/EventInput" } responses: "201": content: application/json: schema: { $ref: "#/components/schemas/CalendarEvent" } /api/v1/events/{id}: parameters: - name: id in: path required: true schema: { type: string, format: uuid } get: summary: Get event tags: [Events] responses: "200": content: application/json: schema: { $ref: "#/components/schemas/CalendarEvent" } patch: summary: Update event tags: [Events] requestBody: content: application/json: schema: allOf: - $ref: "#/components/schemas/EventInput" description: All fields optional for PATCH responses: "200": content: application/json: schema: { $ref: "#/components/schemas/CalendarEvent" } delete: summary: Delete event tags: [Events] responses: "200": content: application/json: schema: { $ref: "#/components/schemas/OkResponse" } /api/v1/lists: get: summary: List lists tags: [Lists] parameters: - name: type in: query schema: { type: string } description: Filter by list type (e.g. shopping, tasks) responses: "200": content: application/json: schema: type: array items: { $ref: "#/components/schemas/ListSummary" } post: summary: Create list tags: [Lists] requestBody: required: true content: application/json: schema: { $ref: "#/components/schemas/ListInput" } responses: "201": content: application/json: schema: { $ref: "#/components/schemas/ListSummary" } /api/v1/lists/{id}: parameters: - name: id in: path required: true schema: { type: string, format: uuid } get: summary: Get list with items tags: [Lists] responses: "200": description: List detail including items array patch: summary: Update list name or archive tags: [Lists] requestBody: content: application/json: schema: type: object properties: name: { type: string } archived: { type: boolean } responses: "200": { description: Updated list } delete: summary: Archive list tags: [Lists] responses: "200": content: application/json: schema: { $ref: "#/components/schemas/OkResponse" } /api/v1/lists/{id}/items: parameters: - name: id in: path required: true schema: { type: string, format: uuid } get: summary: List items tags: [Lists] responses: "200": content: application/json: schema: type: array items: { $ref: "#/components/schemas/ListItem" } post: summary: Add item tags: [Lists] requestBody: required: true content: application/json: schema: { $ref: "#/components/schemas/ItemInput" } responses: "201": content: application/json: schema: { $ref: "#/components/schemas/ListItem" } /api/v1/lists/{id}/items/{itemId}: parameters: - name: id in: path required: true schema: { type: string, format: uuid } - name: itemId in: path required: true schema: { type: string, format: uuid } patch: summary: Update or toggle item tags: [Lists] requestBody: content: application/json: schema: type: object properties: text: { type: string } qty: { type: string, nullable: true } notes: { type: string, nullable: true } done: { type: boolean } dueAt: { type: string, format: date-time, nullable: true } responses: "200": { description: Updated list with items } delete: summary: Delete item tags: [Lists] responses: "200": { description: Updated list with items } /api/v1/notes: get: summary: List notes tags: [Notes] responses: "200": content: application/json: schema: type: array items: { $ref: "#/components/schemas/Note" } post: summary: Create note tags: [Notes] requestBody: required: true content: application/json: schema: { $ref: "#/components/schemas/NoteInput" } responses: "201": content: application/json: schema: { $ref: "#/components/schemas/Note" } /api/v1/notes/{id}: parameters: - name: id in: path required: true schema: { type: string, format: uuid } get: summary: Get note tags: [Notes] responses: "200": content: application/json: schema: { $ref: "#/components/schemas/Note" } patch: summary: Update note tags: [Notes] requestBody: content: application/json: schema: type: object properties: title: { type: string } body: { type: string } pinned: { type: boolean } remindAt: { type: string, format: date-time, nullable: true } responses: "200": content: application/json: schema: { $ref: "#/components/schemas/Note" } delete: summary: Delete note tags: [Notes] responses: "200": content: application/json: schema: { $ref: "#/components/schemas/OkResponse" } /api/v1/garden/containers: get: summary: List garden containers tags: [Garden] responses: "200": content: application/json: schema: type: array items: { $ref: "#/components/schemas/GardenContainer" } post: summary: Create container tags: [Garden] requestBody: required: true content: application/json: schema: { $ref: "#/components/schemas/ContainerInput" } responses: "201": content: application/json: schema: { $ref: "#/components/schemas/GardenContainerDetail" } /api/v1/garden/containers/{id}: parameters: - name: id in: path required: true schema: { type: string, format: uuid } get: summary: Get container with plants tags: [Garden] responses: "200": content: application/json: schema: { $ref: "#/components/schemas/GardenContainerDetail" } "404": { description: Not found } patch: summary: Update container tags: [Garden] requestBody: content: application/json: schema: allOf: - $ref: "#/components/schemas/ContainerInput" description: All fields optional responses: "200": content: application/json: schema: { $ref: "#/components/schemas/GardenContainerDetail" } delete: summary: Delete container tags: [Garden] responses: "200": content: application/json: schema: { $ref: "#/components/schemas/OkResponse" } /api/v1/garden/plants: get: summary: List plants tags: [Garden] parameters: - name: containerId in: query schema: { type: string, format: uuid } description: Filter by container responses: "200": content: application/json: schema: type: array items: { $ref: "#/components/schemas/PlantListItem" } post: summary: Create plant tags: [Garden] requestBody: required: true content: application/json: schema: { $ref: "#/components/schemas/PlantInput" } responses: "201": content: application/json: schema: { $ref: "#/components/schemas/PlantDetail" } /api/v1/garden/plants/{id}: parameters: - name: id in: path required: true schema: { type: string, format: uuid } get: summary: Get plant detail tags: [Garden] responses: "200": content: application/json: schema: { $ref: "#/components/schemas/PlantDetail" } "404": { description: Not found } patch: summary: Update plant tags: [Garden] requestBody: content: application/json: schema: allOf: - $ref: "#/components/schemas/PlantInput" description: All fields optional responses: "200": content: application/json: schema: { $ref: "#/components/schemas/PlantDetail" } delete: summary: Delete plant tags: [Garden] responses: "200": content: application/json: schema: { $ref: "#/components/schemas/OkResponse" } /api/v1/bangs: get: summary: Bang stats (total + recent) tags: [Bangs] parameters: - name: limit in: query schema: { type: integer, default: 10, minimum: 1, maximum: 100 } description: Max recent entries to return responses: "200": content: application/json: schema: { $ref: "#/components/schemas/BangStats" } post: summary: Record a bang tags: [Bangs] requestBody: content: application/json: schema: { $ref: "#/components/schemas/BangInput" } responses: "201": content: application/json: schema: { $ref: "#/components/schemas/Bang" } /api/v1/bangs/{id}: parameters: - name: id in: path required: true schema: { type: string, format: uuid } patch: summary: Update bang date tags: [Bangs] requestBody: required: true content: application/json: schema: { $ref: "#/components/schemas/BangUpdateInput" } responses: "200": content: application/json: schema: { $ref: "#/components/schemas/Bang" } delete: summary: Delete bang tags: [Bangs] responses: "200": content: application/json: schema: { $ref: "#/components/schemas/OkResponse" } /api/v1/journal/entries: get: summary: List journal entries for the authenticated user tags: [Journal] responses: "200": content: application/json: schema: type: array items: { $ref: "#/components/schemas/JournalEntry" } post: summary: Create journal entry tags: [Journal] requestBody: required: true content: application/json: schema: { $ref: "#/components/schemas/JournalEntryInput" } responses: "201": content: application/json: schema: { $ref: "#/components/schemas/JournalEntry" } /api/v1/journal/entries/{id}: parameters: - name: id in: path required: true schema: { type: string, format: uuid } get: summary: Get journal entry tags: [Journal] responses: "200": content: application/json: schema: { $ref: "#/components/schemas/JournalEntry" } patch: summary: Update journal entry tags: [Journal] requestBody: content: application/json: schema: allOf: - $ref: "#/components/schemas/JournalEntryInput" description: All fields optional responses: "200": content: application/json: schema: { $ref: "#/components/schemas/JournalEntry" } delete: summary: Delete journal entry tags: [Journal] responses: "200": content: application/json: schema: { $ref: "#/components/schemas/OkResponse" } tags: - name: Calendars - name: Events - name: Lists - name: Notes - name: Journal - name: Garden - name: Bangs