openapi: 3.0.3 info: title: Relationship Saver API version: 0.1.0 description: | Backend contract for auth, sync, signals, and upload scaffolding. servers: - url: https://api.example.com paths: /v1/auth/sign-in: post: summary: Sign in requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/SignInRequest' responses: '200': description: Signed-in session content: application/json: schema: $ref: '#/components/schemas/AuthSession' /v1/auth/refresh: post: summary: Refresh session requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/AuthRefreshRequest' responses: '200': description: Refreshed token bundle content: application/json: schema: $ref: '#/components/schemas/AuthRefreshResponse' /v1/auth/sign-out: post: summary: Sign out responses: '204': description: Signed out /v1/me: get: summary: Current user profile responses: '200': description: Profile content: application/json: schema: $ref: '#/components/schemas/UserProfile' /v1/sync/push: post: summary: Push local changes parameters: - in: header name: Idempotency-Key schema: type: string requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/SyncPushRequest' responses: '200': description: Push result content: application/json: schema: $ref: '#/components/schemas/SyncPushResult' /v1/sync/pull: get: summary: Pull remote changes parameters: - in: query name: cursor schema: type: string - in: query name: limit schema: type: integer default: 200 responses: '200': description: Pull result content: application/json: schema: $ref: '#/components/schemas/SyncPullResult' /v1/signals: get: summary: Fetch signals feed parameters: - in: query name: cursor schema: type: string - in: query name: since schema: type: string format: date-time - in: query name: limit schema: type: integer default: 50 - in: query name: personId schema: type: array items: type: string responses: '200': description: Signals page content: application/json: schema: $ref: '#/components/schemas/SignalsFeed' /v1/signals/{id}/ack: post: summary: Acknowledge signal parameters: - in: path name: id required: true schema: type: string requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/SignalAckRequest' responses: '204': description: Acknowledged /v1/uploads: post: summary: Initialize upload requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/UploadInitRequest' responses: '200': description: Upload URL content: application/json: schema: $ref: '#/components/schemas/UploadInitResult' /v1/files/{fileId}: get: summary: Resolve file download URL parameters: - in: path name: fileId required: true schema: type: string responses: '200': description: Download URL content: application/json: schema: $ref: '#/components/schemas/DownloadUrl' components: schemas: SignInRequest: type: object properties: method: type: string enum: [email_magic_link, password, oidc] email: type: string format: email password: type: string idToken: type: string required: [method] UserProfile: type: object properties: id: { type: string } email: { type: string, format: email } displayName: { type: string } avatarFileId: { type: string } required: [id] AuthSession: type: object properties: accessToken: { type: string } refreshToken: { type: string } expiresAt: { type: string, format: date-time } user: $ref: '#/components/schemas/UserProfile' required: [accessToken, refreshToken, expiresAt, user] AuthRefreshRequest: type: object properties: refreshToken: { type: string } required: [refreshToken] AuthRefreshResponse: type: object properties: accessToken: { type: string } refreshToken: { type: string } expiresAt: { type: string, format: date-time } required: [accessToken, expiresAt] ChangeEnvelope: type: object properties: schemaVersion: { type: integer } entityType: { type: string } entityId: { type: string } op: type: string enum: [upsert, delete] modifiedAt: type: string format: date-time clientMutationId: { type: string } payload: type: object nullable: true additionalProperties: true required: [schemaVersion, entityType, entityId, op, modifiedAt, clientMutationId] SyncPushRequest: type: object properties: deviceId: { type: string } cursor: { type: string } changes: type: array items: $ref: '#/components/schemas/ChangeEnvelope' idempotencyKey: { type: string } required: [deviceId, changes, idempotencyKey] MutationAck: type: object properties: clientMutationId: { type: string } serverMutationId: { type: string } acceptedAt: { type: string, format: date-time } required: [clientMutationId] MutationRejection: type: object properties: clientMutationId: { type: string } code: { type: string } message: { type: string } required: [clientMutationId, code, message] SyncPushResult: type: object properties: cursor: { type: string } accepted: type: array items: $ref: '#/components/schemas/MutationAck' rejected: type: array items: $ref: '#/components/schemas/MutationRejection' required: [cursor, accepted, rejected] SyncPullResult: type: object properties: cursor: { type: string } changes: type: array items: $ref: '#/components/schemas/ChangeEnvelope' required: [cursor, changes] SignalItem: type: object properties: id: { type: string } type: { type: string } title: { type: string } description: { type: string } personId: { type: string } createdAt: { type: string, format: date-time } metadata: type: object additionalProperties: true required: [id, type, title, createdAt] SignalsFeed: type: object properties: cursor: { type: string } items: type: array items: $ref: '#/components/schemas/SignalItem' required: [items] SignalAckRequest: type: object properties: action: type: string enum: [viewed, dismissed, saved, purchased] at: type: string format: date-time required: [action, at] UploadInitRequest: type: object properties: purpose: type: string enum: [avatar, attachment] contentType: { type: string } sizeBytes: { type: integer } required: [purpose, contentType, sizeBytes] UploadInitResult: type: object properties: uploadUrl: { type: string } fileId: { type: string } expiresAt: { type: string, format: date-time } required: [uploadUrl, fileId, expiresAt] DownloadUrl: type: object properties: downloadUrl: { type: string } expiresAt: { type: string, format: date-time } required: [downloadUrl, expiresAt]