first commit
Made-with: Cursor
This commit is contained in:
899
about/details.md
Normal file
899
about/details.md
Normal file
@@ -0,0 +1,899 @@
|
||||
# Calendar & Contacts API
|
||||
|
||||
## details.md
|
||||
|
||||
This document defines exact endpoint contracts: request/response schemas, field constraints, pagination formats, examples, and implementation notes so a developer can build the API and a frontend without guessing.
|
||||
|
||||
All timestamps are RFC3339 strings.
|
||||
All stored times are UTC.
|
||||
|
||||
---
|
||||
|
||||
# 1. Conventions
|
||||
|
||||
## 1.1 Base URL
|
||||
|
||||
* Local: [http://localhost:8080](http://localhost:8080)
|
||||
* Production: [https://api.example.com](https://api.example.com)
|
||||
|
||||
All endpoints are under the root path.
|
||||
|
||||
---
|
||||
|
||||
## 1.2 Authentication Headers
|
||||
|
||||
JWT:
|
||||
|
||||
* Authorization: Bearer <access_token>
|
||||
|
||||
API key:
|
||||
|
||||
* X-API-Key: <api_key_token>
|
||||
|
||||
If both are present, JWT takes precedence.
|
||||
|
||||
---
|
||||
|
||||
## 1.3 Standard Response Envelope
|
||||
|
||||
This API returns plain JSON objects.
|
||||
For list endpoints, a consistent list envelope is required.
|
||||
|
||||
List response envelope:
|
||||
{
|
||||
"items": [ ... ],
|
||||
"page": {
|
||||
"limit": 50,
|
||||
"next_cursor": "opaque-or-null"
|
||||
}
|
||||
}
|
||||
|
||||
---
|
||||
|
||||
## 1.4 Cursor Pagination
|
||||
|
||||
Query params:
|
||||
|
||||
* limit (optional, default 50, max 200)
|
||||
* cursor (optional)
|
||||
|
||||
Cursor meaning:
|
||||
|
||||
* Opaque base64url string encoding the tuple:
|
||||
|
||||
* last_sort_time (RFC3339)
|
||||
* last_id (uuid)
|
||||
|
||||
Sorting rule for paginated lists:
|
||||
|
||||
* Primary: start_time asc (or created_at for contacts)
|
||||
* Secondary: id asc
|
||||
|
||||
If cursor is provided:
|
||||
|
||||
* Return records strictly greater than the tuple.
|
||||
|
||||
---
|
||||
|
||||
## 1.5 Error Format
|
||||
|
||||
All error responses:
|
||||
{
|
||||
"error": "Human readable message",
|
||||
"code": "MACHINE_READABLE_CODE",
|
||||
"details": "Optional string or object"
|
||||
}
|
||||
|
||||
HTTP mapping:
|
||||
|
||||
* 400 VALIDATION_ERROR
|
||||
* 401 AUTH_REQUIRED / AUTH_INVALID
|
||||
* 403 FORBIDDEN
|
||||
* 404 NOT_FOUND
|
||||
* 409 CONFLICT
|
||||
* 429 RATE_LIMITED
|
||||
* 500 INTERNAL
|
||||
|
||||
---
|
||||
|
||||
# 2. Data Schemas
|
||||
|
||||
## 2.1 User
|
||||
|
||||
{
|
||||
"id": "uuid",
|
||||
"email": "string",
|
||||
"timezone": "string",
|
||||
"created_at": "RFC3339",
|
||||
"updated_at": "RFC3339"
|
||||
}
|
||||
|
||||
Constraints:
|
||||
|
||||
* email lowercase
|
||||
* timezone must be IANA timezone name, default "UTC"
|
||||
|
||||
---
|
||||
|
||||
## 2.2 Calendar
|
||||
|
||||
{
|
||||
"id": "uuid",
|
||||
"name": "string",
|
||||
"color": "string",
|
||||
"is_public": true,
|
||||
"role": "owner|editor|viewer",
|
||||
"created_at": "RFC3339",
|
||||
"updated_at": "RFC3339"
|
||||
}
|
||||
|
||||
Constraints:
|
||||
|
||||
* name 1..80
|
||||
* color is hex like "#RRGGBB" (optional)
|
||||
|
||||
---
|
||||
|
||||
## 2.3 Event
|
||||
|
||||
{
|
||||
"id": "uuid",
|
||||
"calendar_id": "uuid",
|
||||
"title": "string",
|
||||
"description": "string|null",
|
||||
"location": "string|null",
|
||||
"start_time": "RFC3339-UTC",
|
||||
"end_time": "RFC3339-UTC",
|
||||
"timezone": "string",
|
||||
"all_day": false,
|
||||
"recurrence_rule": "string|null",
|
||||
"created_by": "uuid",
|
||||
"updated_by": "uuid",
|
||||
"created_at": "RFC3339",
|
||||
"updated_at": "RFC3339",
|
||||
"reminders": [
|
||||
{"id": "uuid", "minutes_before": 10}
|
||||
],
|
||||
"attendees": [
|
||||
{"id": "uuid", "user_id": "uuid|null", "email": "string|null", "status": "pending|accepted|declined|tentative"}
|
||||
],
|
||||
"tags": ["string"],
|
||||
"attachments": [
|
||||
{"id": "uuid", "file_url": "string"}
|
||||
]
|
||||
}
|
||||
|
||||
Constraints:
|
||||
|
||||
* title 1..140
|
||||
* timezone IANA name
|
||||
* recurrence_rule must be valid RFC5545 RRULE when present
|
||||
|
||||
---
|
||||
|
||||
## 2.4 Contact
|
||||
|
||||
{
|
||||
"id": "uuid",
|
||||
"first_name": "string|null",
|
||||
"last_name": "string|null",
|
||||
"email": "string|null",
|
||||
"phone": "string|null",
|
||||
"company": "string|null",
|
||||
"notes": "string|null",
|
||||
"created_at": "RFC3339",
|
||||
"updated_at": "RFC3339"
|
||||
}
|
||||
|
||||
Constraints:
|
||||
|
||||
* At least one of: first_name, last_name, email, phone must be present
|
||||
|
||||
---
|
||||
|
||||
# 3. Endpoint Contracts
|
||||
|
||||
## 3.1 Auth
|
||||
|
||||
### POST /auth/register
|
||||
|
||||
Request:
|
||||
{
|
||||
"email": "[user@example.com](mailto:user@example.com)",
|
||||
"password": "string",
|
||||
"timezone": "America/Asuncion"
|
||||
}
|
||||
|
||||
Rules:
|
||||
|
||||
* timezone optional
|
||||
* server creates default calendar
|
||||
|
||||
Response 200:
|
||||
{
|
||||
"user": { ...User },
|
||||
"access_token": "string",
|
||||
"refresh_token": "string"
|
||||
}
|
||||
|
||||
Errors:
|
||||
|
||||
* 400 VALIDATION_ERROR
|
||||
* 409 CONFLICT (email already exists)
|
||||
|
||||
---
|
||||
|
||||
### POST /auth/login
|
||||
|
||||
Request:
|
||||
{
|
||||
"email": "[user@example.com](mailto:user@example.com)",
|
||||
"password": "string"
|
||||
}
|
||||
|
||||
Response 200:
|
||||
{
|
||||
"user": { ...User },
|
||||
"access_token": "string",
|
||||
"refresh_token": "string"
|
||||
}
|
||||
|
||||
Errors:
|
||||
|
||||
* 401 AUTH_INVALID
|
||||
|
||||
---
|
||||
|
||||
### POST /auth/refresh
|
||||
|
||||
Request:
|
||||
{
|
||||
"refresh_token": "string"
|
||||
}
|
||||
|
||||
Response 200:
|
||||
{
|
||||
"access_token": "string",
|
||||
"refresh_token": "string"
|
||||
}
|
||||
|
||||
Errors:
|
||||
|
||||
* 401 AUTH_INVALID
|
||||
|
||||
---
|
||||
|
||||
### POST /auth/logout
|
||||
|
||||
Request:
|
||||
{
|
||||
"refresh_token": "string"
|
||||
}
|
||||
|
||||
Response 200:
|
||||
{
|
||||
"ok": true
|
||||
}
|
||||
|
||||
---
|
||||
|
||||
### GET /auth/me
|
||||
|
||||
Response 200:
|
||||
{
|
||||
"user": { ...User }
|
||||
}
|
||||
|
||||
---
|
||||
|
||||
## 3.2 API Keys
|
||||
|
||||
### POST /api-keys
|
||||
|
||||
Request:
|
||||
{
|
||||
"name": "My agent key",
|
||||
"scopes": {
|
||||
"calendars": ["read", "write"],
|
||||
"events": ["read", "write"],
|
||||
"contacts": ["read", "write"],
|
||||
"availability": ["read"],
|
||||
"booking": ["write"]
|
||||
}
|
||||
}
|
||||
|
||||
Response 200:
|
||||
{
|
||||
"id": "uuid",
|
||||
"name": "My agent key",
|
||||
"created_at": "RFC3339",
|
||||
"token": "RAW_TOKEN_RETURNED_ONCE"
|
||||
}
|
||||
|
||||
---
|
||||
|
||||
### GET /api-keys
|
||||
|
||||
Response 200:
|
||||
{
|
||||
"items": [
|
||||
{"id": "uuid", "name": "string", "created_at": "RFC3339", "revoked_at": "RFC3339|null"}
|
||||
],
|
||||
"page": {"limit": 50, "next_cursor": null}
|
||||
}
|
||||
|
||||
---
|
||||
|
||||
### DELETE /api-keys/{id}
|
||||
|
||||
Response 200:
|
||||
{
|
||||
"ok": true
|
||||
}
|
||||
|
||||
---
|
||||
|
||||
## 3.3 Users
|
||||
|
||||
### GET /users/me
|
||||
|
||||
Response 200:
|
||||
{
|
||||
"user": { ...User }
|
||||
}
|
||||
|
||||
---
|
||||
|
||||
### PUT /users/me
|
||||
|
||||
Request:
|
||||
{
|
||||
"timezone": "America/Asuncion"
|
||||
}
|
||||
|
||||
Response 200:
|
||||
{
|
||||
"user": { ...User }
|
||||
}
|
||||
|
||||
---
|
||||
|
||||
### DELETE /users/me
|
||||
|
||||
Response 200:
|
||||
{
|
||||
"ok": true
|
||||
}
|
||||
|
||||
---
|
||||
|
||||
## 3.4 Calendars
|
||||
|
||||
### GET /calendars
|
||||
|
||||
Response 200:
|
||||
{
|
||||
"items": [ ...Calendar ],
|
||||
"page": {"limit": 50, "next_cursor": null}
|
||||
}
|
||||
|
||||
---
|
||||
|
||||
### POST /calendars
|
||||
|
||||
Request:
|
||||
{
|
||||
"name": "Work",
|
||||
"color": "#22C55E"
|
||||
}
|
||||
|
||||
Response 200:
|
||||
{
|
||||
"calendar": { ...Calendar }
|
||||
}
|
||||
|
||||
---
|
||||
|
||||
### GET /calendars/{id}
|
||||
|
||||
Response 200:
|
||||
{
|
||||
"calendar": { ...Calendar }
|
||||
}
|
||||
|
||||
---
|
||||
|
||||
### PUT /calendars/{id}
|
||||
|
||||
Request:
|
||||
{
|
||||
"name": "Work Calendar",
|
||||
"color": "#22C55E",
|
||||
"is_public": false
|
||||
}
|
||||
|
||||
Rules:
|
||||
|
||||
* is_public only owner
|
||||
|
||||
Response 200:
|
||||
{
|
||||
"calendar": { ...Calendar }
|
||||
}
|
||||
|
||||
---
|
||||
|
||||
### DELETE /calendars/{id}
|
||||
|
||||
Response 200:
|
||||
{
|
||||
"ok": true
|
||||
}
|
||||
|
||||
---
|
||||
|
||||
## 3.5 Calendar Sharing
|
||||
|
||||
### POST /calendars/{id}/share
|
||||
|
||||
Request:
|
||||
{
|
||||
"target": {"email": "[other@example.com](mailto:other@example.com)"},
|
||||
"role": "editor"
|
||||
}
|
||||
|
||||
Response 200:
|
||||
{
|
||||
"ok": true
|
||||
}
|
||||
|
||||
---
|
||||
|
||||
### GET /calendars/{id}/members
|
||||
|
||||
Response 200:
|
||||
{
|
||||
"items": [
|
||||
{"user_id": "uuid", "email": "string", "role": "owner|editor|viewer"}
|
||||
],
|
||||
"page": {"limit": 50, "next_cursor": null}
|
||||
}
|
||||
|
||||
---
|
||||
|
||||
### DELETE /calendars/{id}/members/{user_id}
|
||||
|
||||
Response 200:
|
||||
{
|
||||
"ok": true
|
||||
}
|
||||
|
||||
---
|
||||
|
||||
## 3.6 Events
|
||||
|
||||
### GET /events
|
||||
|
||||
Query:
|
||||
|
||||
* start (required) RFC3339
|
||||
* end (required) RFC3339
|
||||
* calendar_id (optional)
|
||||
* search (optional)
|
||||
* tag (optional)
|
||||
* limit, cursor
|
||||
|
||||
Response 200:
|
||||
{
|
||||
"items": [ ...Event ],
|
||||
"page": {"limit": 50, "next_cursor": "string|null"}
|
||||
}
|
||||
|
||||
Notes:
|
||||
|
||||
* Must include expanded recurrence occurrences inside requested range.
|
||||
* For recurrence expansion, include occurrences as separate items with:
|
||||
|
||||
* id = master event id
|
||||
* occurrence_start_time, occurrence_end_time (recommended fields)
|
||||
|
||||
Recommended recurrence occurrence representation:
|
||||
{
|
||||
"id": "uuid-master",
|
||||
"is_occurrence": true,
|
||||
"occurrence_start_time": "RFC3339",
|
||||
"occurrence_end_time": "RFC3339",
|
||||
...base event fields...
|
||||
}
|
||||
|
||||
---
|
||||
|
||||
### POST /events
|
||||
|
||||
Request:
|
||||
{
|
||||
"calendar_id": "uuid",
|
||||
"title": "Meeting",
|
||||
"description": "Project sync",
|
||||
"location": "Zoom",
|
||||
"start_time": "2026-03-01T14:00:00-03:00",
|
||||
"end_time": "2026-03-01T15:00:00-03:00",
|
||||
"timezone": "America/Asuncion",
|
||||
"all_day": false,
|
||||
"recurrence_rule": null,
|
||||
"reminders": [10, 60],
|
||||
"tags": ["work", "sync"]
|
||||
}
|
||||
|
||||
Response 200:
|
||||
{
|
||||
"event": { ...Event }
|
||||
}
|
||||
|
||||
Errors:
|
||||
|
||||
* 403 FORBIDDEN
|
||||
* 400 VALIDATION_ERROR
|
||||
|
||||
---
|
||||
|
||||
### GET /events/{id}
|
||||
|
||||
Response 200:
|
||||
{
|
||||
"event": { ...Event }
|
||||
}
|
||||
|
||||
---
|
||||
|
||||
### PUT /events/{id}
|
||||
|
||||
Request:
|
||||
{
|
||||
"title": "Updated title",
|
||||
"start_time": "2026-03-01T15:00:00-03:00",
|
||||
"end_time": "2026-03-01T16:00:00-03:00",
|
||||
"recurrence_rule": "FREQ=WEEKLY;BYDAY=MO,WE,FR"
|
||||
}
|
||||
|
||||
Response 200:
|
||||
{
|
||||
"event": { ...Event }
|
||||
}
|
||||
|
||||
---
|
||||
|
||||
### DELETE /events/{id}
|
||||
|
||||
Response 200:
|
||||
{
|
||||
"ok": true
|
||||
}
|
||||
|
||||
---
|
||||
|
||||
## 3.7 Event Reminders
|
||||
|
||||
### POST /events/{id}/reminders
|
||||
|
||||
Request:
|
||||
{
|
||||
"minutes_before": [5, 15, 60]
|
||||
}
|
||||
|
||||
Response 200:
|
||||
{
|
||||
"event": { ...Event }
|
||||
}
|
||||
|
||||
---
|
||||
|
||||
### DELETE /events/{id}/reminders/{reminder_id}
|
||||
|
||||
Response 200:
|
||||
{
|
||||
"ok": true
|
||||
}
|
||||
|
||||
---
|
||||
|
||||
## 3.8 Attendees
|
||||
|
||||
### POST /events/{id}/attendees
|
||||
|
||||
Request:
|
||||
{
|
||||
"attendees": [
|
||||
{"email": "[guest@example.com](mailto:guest@example.com)"},
|
||||
{"user_id": "uuid"}
|
||||
]
|
||||
}
|
||||
|
||||
Response 200:
|
||||
{
|
||||
"event": { ...Event }
|
||||
}
|
||||
|
||||
---
|
||||
|
||||
### PUT /events/{id}/attendees/{attendee_id}
|
||||
|
||||
Request:
|
||||
{
|
||||
"status": "accepted"
|
||||
}
|
||||
|
||||
Rules:
|
||||
|
||||
* Organizer can edit any attendee
|
||||
* Attendee can edit own status
|
||||
|
||||
Response 200:
|
||||
{
|
||||
"event": { ...Event }
|
||||
}
|
||||
|
||||
---
|
||||
|
||||
### DELETE /events/{id}/attendees/{attendee_id}
|
||||
|
||||
Response 200:
|
||||
{
|
||||
"ok": true
|
||||
}
|
||||
|
||||
---
|
||||
|
||||
## 3.9 Contacts
|
||||
|
||||
### GET /contacts
|
||||
|
||||
Query:
|
||||
|
||||
* search (optional)
|
||||
* limit, cursor
|
||||
|
||||
Response 200:
|
||||
{
|
||||
"items": [ ...Contact ],
|
||||
"page": {"limit": 50, "next_cursor": "string|null"}
|
||||
}
|
||||
|
||||
---
|
||||
|
||||
### POST /contacts
|
||||
|
||||
Request:
|
||||
{
|
||||
"first_name": "Jane",
|
||||
"last_name": "Doe",
|
||||
"email": "[jane@example.com](mailto:jane@example.com)",
|
||||
"phone": "+595981000000",
|
||||
"company": "Example SA",
|
||||
"notes": "Met at event"
|
||||
}
|
||||
|
||||
Response 200:
|
||||
{
|
||||
"contact": { ...Contact }
|
||||
}
|
||||
|
||||
---
|
||||
|
||||
### GET /contacts/{id}
|
||||
|
||||
Response 200:
|
||||
{
|
||||
"contact": { ...Contact }
|
||||
}
|
||||
|
||||
---
|
||||
|
||||
### PUT /contacts/{id}
|
||||
|
||||
Request:
|
||||
{
|
||||
"notes": "Updated notes"
|
||||
}
|
||||
|
||||
Response 200:
|
||||
{
|
||||
"contact": { ...Contact }
|
||||
}
|
||||
|
||||
---
|
||||
|
||||
### DELETE /contacts/{id}
|
||||
|
||||
Response 200:
|
||||
{
|
||||
"ok": true
|
||||
}
|
||||
|
||||
---
|
||||
|
||||
## 3.10 Availability
|
||||
|
||||
### GET /availability
|
||||
|
||||
Query:
|
||||
|
||||
* calendar_id (required)
|
||||
* start (required)
|
||||
* end (required)
|
||||
|
||||
Response 200:
|
||||
{
|
||||
"calendar_id": "uuid",
|
||||
"range_start": "RFC3339",
|
||||
"range_end": "RFC3339",
|
||||
"busy": [
|
||||
{"start": "RFC3339", "end": "RFC3339", "event_id": "uuid"}
|
||||
]
|
||||
}
|
||||
|
||||
---
|
||||
|
||||
## 3.11 Booking Links
|
||||
|
||||
### POST /calendars/{id}/booking-link
|
||||
|
||||
Request:
|
||||
{
|
||||
"duration_minutes": 30,
|
||||
"buffer_minutes": 0,
|
||||
"timezone": "America/Asuncion",
|
||||
"working_hours": {
|
||||
"mon": [{"start": "09:00", "end": "17:00"}],
|
||||
"tue": [{"start": "09:00", "end": "17:00"}],
|
||||
"wed": [{"start": "09:00", "end": "17:00"}],
|
||||
"thu": [{"start": "09:00", "end": "17:00"}],
|
||||
"fri": [{"start": "09:00", "end": "17:00"}],
|
||||
"sat": [],
|
||||
"sun": []
|
||||
},
|
||||
"active": true
|
||||
}
|
||||
|
||||
Response 200:
|
||||
{
|
||||
"token": "string",
|
||||
"public_url": "[https://app.example.com/booking/](https://app.example.com/booking/)<token>",
|
||||
"settings": { ...same-as-request }
|
||||
}
|
||||
|
||||
---
|
||||
|
||||
### GET /booking/{token}/availability
|
||||
|
||||
Query:
|
||||
|
||||
* start
|
||||
* end
|
||||
|
||||
Response 200:
|
||||
{
|
||||
"token": "string",
|
||||
"timezone": "America/Asuncion",
|
||||
"duration_minutes": 30,
|
||||
"slots": [
|
||||
{"start": "RFC3339", "end": "RFC3339"}
|
||||
]
|
||||
}
|
||||
|
||||
---
|
||||
|
||||
### POST /booking/{token}/reserve
|
||||
|
||||
Request:
|
||||
{
|
||||
"name": "Visitor Name",
|
||||
"email": "[visitor@example.com](mailto:visitor@example.com)",
|
||||
"slot_start": "RFC3339",
|
||||
"slot_end": "RFC3339",
|
||||
"notes": "Optional"
|
||||
}
|
||||
|
||||
Response 200:
|
||||
{
|
||||
"ok": true,
|
||||
"event": { ...Event }
|
||||
}
|
||||
|
||||
Errors:
|
||||
|
||||
* 409 CONFLICT if slot no longer available
|
||||
|
||||
---
|
||||
|
||||
## 3.12 ICS
|
||||
|
||||
### GET /calendars/{id}/export.ics
|
||||
|
||||
Response:
|
||||
|
||||
* Content-Type: text/calendar
|
||||
* Body: ICS format
|
||||
|
||||
---
|
||||
|
||||
### POST /calendars/import
|
||||
|
||||
Request:
|
||||
|
||||
* multipart/form-data
|
||||
|
||||
* calendar_id (uuid)
|
||||
* file (.ics)
|
||||
|
||||
Response 200:
|
||||
{
|
||||
"ok": true,
|
||||
"imported": {
|
||||
"events": 12
|
||||
}
|
||||
}
|
||||
|
||||
---
|
||||
|
||||
# 4. Validation Constraints Summary
|
||||
|
||||
Users:
|
||||
|
||||
* email unique
|
||||
* password >=10 chars
|
||||
|
||||
Calendars:
|
||||
|
||||
* name 1..80
|
||||
* color valid hex
|
||||
|
||||
Events:
|
||||
|
||||
* title 1..140
|
||||
* end_time > start_time
|
||||
* timezone valid IANA
|
||||
* reminders minutes_before must be 0..10080
|
||||
|
||||
Contacts:
|
||||
|
||||
* at least one identifying field
|
||||
|
||||
---
|
||||
|
||||
# 5. Implementation Notes (Go)
|
||||
|
||||
## Handler Layer
|
||||
|
||||
* Parse JSON
|
||||
* Validate basic constraints
|
||||
* Pass to service
|
||||
|
||||
## Service Layer
|
||||
|
||||
* Permission enforcement
|
||||
* Ownership validation
|
||||
* Time conversion to UTC
|
||||
* Recurrence validation and expansion
|
||||
* Reminder job scheduling
|
||||
* Transaction management for booking reservations
|
||||
|
||||
## Repository Layer
|
||||
|
||||
* sqlc queries
|
||||
* No business logic
|
||||
|
||||
---
|
||||
|
||||
# 6. Frontend and Agent Integration Guarantees
|
||||
|
||||
* The API must remain consistent in response shape.
|
||||
* List endpoints always return items + page.
|
||||
* All objects include created_at/updated_at.
|
||||
* Calendar list includes role.
|
||||
* Event list returns occurrences for recurrence within range.
|
||||
* Booking endpoints require no auth.
|
||||
|
||||
---
|
||||
|
||||
End of details.md
|
||||
501
about/logic.md
Normal file
501
about/logic.md
Normal file
@@ -0,0 +1,501 @@
|
||||
# Calendar & Contacts API
|
||||
|
||||
## logic.md
|
||||
|
||||
This document defines the COMPLETE business logic layer of the Calendar & Contacts API.
|
||||
|
||||
It explains:
|
||||
|
||||
* Permission enforcement
|
||||
* Ownership rules
|
||||
* Validation rules
|
||||
* Recurrence engine behavior
|
||||
* Reminder processing
|
||||
* Availability calculation
|
||||
* Booking logic
|
||||
* Transaction boundaries
|
||||
* Audit requirements
|
||||
* Edge case handling
|
||||
|
||||
This file MUST be treated as the authoritative source for backend logic.
|
||||
|
||||
---
|
||||
|
||||
## GLOBAL INVARIANTS
|
||||
|
||||
1. All timestamps stored in UTC.
|
||||
2. All API timestamps are RFC3339 strings.
|
||||
3. Ownership is ALWAYS derived from auth context.
|
||||
4. Soft deletes are enforced everywhere.
|
||||
5. No endpoint may leak data across users.
|
||||
6. All mutations must write audit log entries.
|
||||
|
||||
---
|
||||
|
||||
1. AUTHENTICATION & CONTEXT
|
||||
|
||||
---
|
||||
|
||||
Every authenticated request must result in a RequestContext struct:
|
||||
|
||||
RequestContext:
|
||||
|
||||
* user_id (uuid)
|
||||
* auth_method ("jwt" | "api_key")
|
||||
* scopes (if api_key)
|
||||
|
||||
JWT auth:
|
||||
|
||||
* Validate signature
|
||||
* Validate expiration
|
||||
* Extract user_id
|
||||
|
||||
API key auth:
|
||||
|
||||
* Hash provided key
|
||||
* Lookup in api_keys
|
||||
* Ensure revoked_at is NULL
|
||||
* Load scopes
|
||||
|
||||
If neither provided → AUTH_REQUIRED
|
||||
|
||||
If invalid → AUTH_INVALID
|
||||
|
||||
---
|
||||
|
||||
2. PERMISSION MODEL
|
||||
|
||||
---
|
||||
|
||||
Calendar roles:
|
||||
|
||||
* owner
|
||||
* editor
|
||||
* viewer
|
||||
|
||||
Permission matrix:
|
||||
|
||||
CALENDAR ACTIONS
|
||||
|
||||
* owner: full access
|
||||
* editor: read calendar, CRUD events
|
||||
* viewer: read-only
|
||||
|
||||
EVENT ACTIONS
|
||||
|
||||
* owner/editor: create/update/delete
|
||||
* viewer: read
|
||||
|
||||
CONTACT ACTIONS
|
||||
|
||||
* Only owner of contacts can CRUD
|
||||
|
||||
BOOKING
|
||||
|
||||
* Only owner can create booking links
|
||||
|
||||
API KEY SCOPE ENFORCEMENT
|
||||
Each endpoint maps to required scope.
|
||||
Example:
|
||||
|
||||
* GET /calendars → calendars:read
|
||||
* POST /events → events:write
|
||||
* GET /contacts → contacts:read
|
||||
|
||||
If scope missing → FORBIDDEN
|
||||
|
||||
---
|
||||
|
||||
3. USER LOGIC
|
||||
|
||||
---
|
||||
|
||||
REGISTER
|
||||
|
||||
* Email lowercase
|
||||
* Must be unique
|
||||
* Password >=10 chars
|
||||
* Hash with bcrypt cost >=12
|
||||
* Create default calendar
|
||||
* Return tokens
|
||||
|
||||
DELETE USER
|
||||
|
||||
* Soft delete user
|
||||
* Soft delete calendars
|
||||
* Soft delete events
|
||||
* Soft delete contacts
|
||||
* Revoke API keys
|
||||
|
||||
---
|
||||
|
||||
4. CALENDAR LOGIC
|
||||
|
||||
---
|
||||
|
||||
CREATE
|
||||
|
||||
* owner_id = authenticated user
|
||||
* name required (1..80)
|
||||
* color optional hex validation
|
||||
|
||||
LIST
|
||||
Return calendars where:
|
||||
|
||||
* owner_id = user_id
|
||||
OR
|
||||
* calendar_members.user_id = user_id
|
||||
|
||||
Include role in response.
|
||||
|
||||
DELETE
|
||||
|
||||
* Only owner
|
||||
* Soft delete calendar
|
||||
* Soft delete all related events
|
||||
|
||||
SHARING
|
||||
|
||||
* Only owner can share
|
||||
* Cannot share with self
|
||||
* Upsert membership row
|
||||
|
||||
REMOVE MEMBER
|
||||
|
||||
* Only owner
|
||||
* Cannot remove owner
|
||||
|
||||
---
|
||||
|
||||
5. EVENT LOGIC
|
||||
|
||||
---
|
||||
|
||||
CREATE EVENT
|
||||
Validation:
|
||||
|
||||
* calendar exists
|
||||
* user has editor or owner role
|
||||
* title 1..140
|
||||
* end_time > start_time
|
||||
* timezone valid IANA
|
||||
|
||||
Time handling:
|
||||
|
||||
* Convert start_time to UTC
|
||||
* Convert end_time to UTC
|
||||
* Store original timezone string
|
||||
|
||||
Overlap rule:
|
||||
|
||||
* Overlap allowed by default
|
||||
* Booking system enforces no-overlap
|
||||
|
||||
If recurrence_rule provided:
|
||||
|
||||
* Validate via rrule-go
|
||||
* Store string
|
||||
|
||||
If reminders provided:
|
||||
|
||||
* Insert reminders
|
||||
* Schedule jobs
|
||||
|
||||
UPDATE EVENT
|
||||
|
||||
* Same permission check
|
||||
* Re-validate time constraints
|
||||
* If recurrence changed → validate again
|
||||
* Reschedule reminders
|
||||
|
||||
DELETE EVENT
|
||||
|
||||
* Soft delete
|
||||
|
||||
---
|
||||
|
||||
6. RECURRENCE ENGINE
|
||||
|
||||
---
|
||||
|
||||
Recurring events are NOT pre-expanded.
|
||||
|
||||
Storage:
|
||||
|
||||
* recurrence_rule string on master event
|
||||
|
||||
Expansion occurs ONLY during:
|
||||
|
||||
* GET /events
|
||||
* GET /events/{id}/occurrences
|
||||
|
||||
Algorithm:
|
||||
|
||||
For each event in DB where:
|
||||
|
||||
* event.start_time <= range_end
|
||||
|
||||
If no recurrence_rule:
|
||||
|
||||
* Include if intersects range
|
||||
|
||||
If recurrence_rule present:
|
||||
|
||||
* Initialize rrule with DTSTART = event.start_time
|
||||
* Generate occurrences within [range_start, range_end]
|
||||
* For each occurrence:
|
||||
|
||||
* Check against exceptions
|
||||
* Create virtual occurrence object
|
||||
|
||||
Occurrence response must include:
|
||||
|
||||
* is_occurrence = true
|
||||
* occurrence_start_time
|
||||
* occurrence_end_time
|
||||
|
||||
Exceptions table:
|
||||
|
||||
* event_id
|
||||
* exception_date
|
||||
* action ("skip")
|
||||
|
||||
If occurrence matches exception_date → skip
|
||||
|
||||
---
|
||||
|
||||
7. REMINDER PROCESSING
|
||||
|
||||
---
|
||||
|
||||
Reminder scheduling rule:
|
||||
|
||||
For each reminder (minutes_before):
|
||||
trigger_time = event.start_time - minutes_before
|
||||
|
||||
If trigger_time > now:
|
||||
|
||||
* Enqueue Asynq job
|
||||
|
||||
Job payload:
|
||||
|
||||
* event_id
|
||||
* reminder_id
|
||||
* user_id
|
||||
|
||||
Worker logic:
|
||||
|
||||
* Load event
|
||||
* If event.deleted_at != NULL → abort
|
||||
* Send notification (webhook/email placeholder)
|
||||
* Retry on failure with exponential backoff
|
||||
|
||||
On event update:
|
||||
|
||||
* Cancel old reminder jobs (if supported)
|
||||
* Recompute schedule
|
||||
|
||||
---
|
||||
|
||||
8. CONTACT LOGIC
|
||||
|
||||
---
|
||||
|
||||
Contacts are strictly per-user.
|
||||
|
||||
CREATE
|
||||
|
||||
* Must have at least one identifying field
|
||||
* email validated if present
|
||||
|
||||
SEARCH
|
||||
|
||||
* Case-insensitive match on:
|
||||
|
||||
* first_name
|
||||
* last_name
|
||||
* email
|
||||
* company
|
||||
|
||||
DELETE
|
||||
|
||||
* Soft delete
|
||||
|
||||
---
|
||||
|
||||
9. AVAILABILITY LOGIC
|
||||
|
||||
---
|
||||
|
||||
Endpoint requires:
|
||||
|
||||
* calendar_id
|
||||
* start
|
||||
* end
|
||||
|
||||
Permission:
|
||||
|
||||
* viewer or higher
|
||||
|
||||
Busy condition:
|
||||
An event is busy if:
|
||||
|
||||
(event.start_time < range_end)
|
||||
AND
|
||||
(event.end_time > range_start)
|
||||
|
||||
Recurring events:
|
||||
|
||||
* Expand occurrences
|
||||
* Apply same intersection rule
|
||||
|
||||
Return busy blocks sorted by start.
|
||||
|
||||
---
|
||||
|
||||
10. BOOKING SYSTEM LOGIC
|
||||
|
||||
---
|
||||
|
||||
CREATE BOOKING LINK
|
||||
|
||||
* Only owner
|
||||
* Generate secure random token
|
||||
* Store configuration
|
||||
|
||||
PUBLIC AVAILABILITY
|
||||
|
||||
* No auth
|
||||
* Load booking link
|
||||
* Validate active
|
||||
* Compute working-hour windows
|
||||
* Subtract busy blocks
|
||||
* Apply buffer_minutes before/after events
|
||||
|
||||
RESERVATION
|
||||
|
||||
* Begin DB transaction
|
||||
* Re-check slot availability with overlap query
|
||||
* If conflict → ROLLBACK + CONFLICT
|
||||
* Insert event
|
||||
* Commit
|
||||
|
||||
Overlap query inside transaction:
|
||||
|
||||
SELECT 1 FROM events
|
||||
WHERE calendar_id = ?
|
||||
AND deleted_at IS NULL
|
||||
AND start_time < slot_end
|
||||
AND end_time > slot_start
|
||||
FOR UPDATE
|
||||
|
||||
This ensures race safety.
|
||||
|
||||
---
|
||||
|
||||
11. CURSOR PAGINATION
|
||||
|
||||
---
|
||||
|
||||
Cursor format:
|
||||
base64url(last_sort_time|last_id)
|
||||
|
||||
Decoding:
|
||||
|
||||
* Split by pipe
|
||||
* Use as tuple comparison
|
||||
|
||||
Events sorting:
|
||||
ORDER BY start_time ASC, id ASC
|
||||
|
||||
Contacts sorting:
|
||||
ORDER BY created_at ASC, id ASC
|
||||
|
||||
Limit enforcement:
|
||||
|
||||
* Default 50
|
||||
* Max 200
|
||||
|
||||
---
|
||||
|
||||
12. AUDIT LOG LOGIC
|
||||
|
||||
---
|
||||
|
||||
Every mutation writes:
|
||||
|
||||
entity_type
|
||||
entity_id
|
||||
action
|
||||
user_id
|
||||
timestamp
|
||||
|
||||
Actions examples:
|
||||
|
||||
* CREATE_EVENT
|
||||
* UPDATE_EVENT
|
||||
* DELETE_EVENT
|
||||
* SHARE_CALENDAR
|
||||
* DELETE_CONTACT
|
||||
|
||||
Audit writes MUST NOT fail main transaction.
|
||||
If audit insert fails:
|
||||
|
||||
* Log error
|
||||
* Continue response
|
||||
|
||||
---
|
||||
|
||||
13. ERROR RULES
|
||||
|
||||
---
|
||||
|
||||
AUTH_REQUIRED → no credentials
|
||||
AUTH_INVALID → invalid token
|
||||
FORBIDDEN → permission denied
|
||||
NOT_FOUND → entity not found or not accessible
|
||||
VALIDATION_ERROR → invalid input
|
||||
CONFLICT → overlap or duplicate
|
||||
INTERNAL → unexpected error
|
||||
|
||||
Never expose internal DB errors directly.
|
||||
|
||||
---
|
||||
|
||||
14. TRANSACTION RULES
|
||||
|
||||
---
|
||||
|
||||
Transactions REQUIRED for:
|
||||
|
||||
* Booking reservation
|
||||
* Event creation with reminders
|
||||
* Event update affecting reminders
|
||||
* Deleting calendar (cascade soft delete)
|
||||
|
||||
Service layer must manage transactions.
|
||||
Repository layer must not auto-commit.
|
||||
|
||||
---
|
||||
|
||||
15. PERFORMANCE REQUIREMENTS
|
||||
|
||||
---
|
||||
|
||||
All list endpoints:
|
||||
|
||||
* Must use indexed columns
|
||||
* Must use pagination
|
||||
* Must avoid N+1 queries
|
||||
|
||||
Recurring expansion must be bounded by requested range.
|
||||
|
||||
Maximum recurrence expansion window allowed per request:
|
||||
|
||||
* 1 year (recommended safeguard)
|
||||
|
||||
If range exceeds safeguard → VALIDATION_ERROR
|
||||
|
||||
---
|
||||
|
||||
This is the authoritative backend logic specification.
|
||||
389
about/overview.md
Normal file
389
about/overview.md
Normal file
@@ -0,0 +1,389 @@
|
||||
# Calendar & Contacts API
|
||||
|
||||
## 1. Purpose
|
||||
|
||||
This system is a production‑grade Calendar and Contacts REST API written in Go. It is designed for:
|
||||
|
||||
* Human users (web/mobile frontends)
|
||||
* AI agents (programmatic automation)
|
||||
* Future SaaS expansion
|
||||
* High‑integrity multi‑user environments
|
||||
|
||||
The API must be stateless, secure, permission‑enforced, timezone‑safe, and scalable.
|
||||
|
||||
This document defines EXACTLY what must be built.
|
||||
|
||||
---
|
||||
|
||||
# 2. System Architecture
|
||||
|
||||
Client (Web / Mobile / Agent)
|
||||
↓
|
||||
HTTP REST API (Go + Chi)
|
||||
↓
|
||||
Service Layer (Business Logic)
|
||||
↓
|
||||
Repository Layer (SQL via sqlc)
|
||||
↓
|
||||
PostgreSQL
|
||||
|
||||
Optional components:
|
||||
|
||||
* Redis (rate limiting + job queue)
|
||||
* Background Worker (reminders)
|
||||
* S3-compatible storage (attachments)
|
||||
* WebSocket server (real-time updates)
|
||||
|
||||
---
|
||||
|
||||
# 3. Core Design Principles
|
||||
|
||||
1. Stateless authentication (JWT or API key)
|
||||
2. Strict ownership validation
|
||||
3. All timestamps stored in UTC
|
||||
4. RFC3339 format for API
|
||||
5. Soft deletion instead of hard deletion
|
||||
6. No trust in client-provided ownership fields
|
||||
7. Clear separation between handlers, services, repositories
|
||||
8. Indexes on all high-query columns
|
||||
|
||||
---
|
||||
|
||||
# 4. Technology Stack
|
||||
|
||||
Language: Go 1.22+
|
||||
Router: Chi
|
||||
Database: PostgreSQL 15+
|
||||
Query Layer: sqlc
|
||||
Migrations: golang-migrate
|
||||
Auth: JWT (github.com/golang-jwt/jwt/v5)
|
||||
Password hashing: bcrypt
|
||||
UUID: google/uuid
|
||||
Background jobs: Asynq (Redis)
|
||||
WebSockets: gorilla/websocket (optional)
|
||||
RRULE: rrule-go
|
||||
Storage: S3 compatible (MinIO client)
|
||||
|
||||
---
|
||||
|
||||
# 5. Authentication Model
|
||||
|
||||
Two authentication modes must be implemented.
|
||||
|
||||
## 5.1 User Authentication
|
||||
|
||||
* Email + password
|
||||
* Password hashed with bcrypt (cost 12+)
|
||||
* JWT access token (15 min expiration)
|
||||
* Refresh token (7–30 days)
|
||||
|
||||
JWT payload:
|
||||
{
|
||||
"user_id": "uuid",
|
||||
"exp": unix_timestamp
|
||||
}
|
||||
|
||||
All protected endpoints require:
|
||||
Authorization: Bearer <token>
|
||||
|
||||
Middleware must:
|
||||
|
||||
* Validate signature
|
||||
* Validate expiration
|
||||
* Inject user_id into request context
|
||||
|
||||
---
|
||||
|
||||
## 5.2 Agent Authentication (API Keys)
|
||||
|
||||
Agents must be able to:
|
||||
|
||||
* Create account
|
||||
* Generate API key
|
||||
* Perform scoped operations
|
||||
|
||||
API keys:
|
||||
|
||||
* Random 32+ byte token
|
||||
* Only hash stored in DB
|
||||
* Sent via header:
|
||||
X-API-Key: <token>
|
||||
|
||||
Scopes example:
|
||||
{
|
||||
"calendars": ["read", "write"],
|
||||
"events": ["read", "write"],
|
||||
"contacts": ["read", "write"]
|
||||
}
|
||||
|
||||
Middleware must validate scope before allowing access.
|
||||
|
||||
---
|
||||
|
||||
# 6. Data Model Overview
|
||||
|
||||
## Users
|
||||
|
||||
* id (uuid)
|
||||
* email (unique)
|
||||
* password_hash
|
||||
* timezone
|
||||
* is_active
|
||||
* created_at
|
||||
* updated_at
|
||||
* deleted_at
|
||||
|
||||
## API Keys
|
||||
|
||||
* id (uuid)
|
||||
* user_id
|
||||
* name
|
||||
* key_hash
|
||||
* scopes (jsonb)
|
||||
* created_at
|
||||
* revoked_at
|
||||
|
||||
## Calendars
|
||||
|
||||
* id (uuid)
|
||||
* owner_id
|
||||
* name
|
||||
* color
|
||||
* is_public
|
||||
* public_token
|
||||
* created_at
|
||||
* updated_at
|
||||
* deleted_at
|
||||
|
||||
## Calendar Members
|
||||
|
||||
* calendar_id
|
||||
* user_id
|
||||
* role (owner/editor/viewer)
|
||||
|
||||
## Events
|
||||
|
||||
* id (uuid)
|
||||
* calendar_id
|
||||
* title
|
||||
* description
|
||||
* location
|
||||
* start_time (UTC)
|
||||
* end_time (UTC)
|
||||
* timezone
|
||||
* all_day
|
||||
* recurrence_rule
|
||||
* created_by
|
||||
* updated_by
|
||||
* created_at
|
||||
* updated_at
|
||||
* deleted_at
|
||||
|
||||
## Event Reminders
|
||||
|
||||
* id
|
||||
* event_id
|
||||
* minutes_before
|
||||
|
||||
## Event Attendees
|
||||
|
||||
* id
|
||||
* event_id
|
||||
* user_id (nullable)
|
||||
* email (nullable)
|
||||
* status
|
||||
|
||||
## Contacts
|
||||
|
||||
* id
|
||||
* owner_id
|
||||
* first_name
|
||||
* last_name
|
||||
* email
|
||||
* phone
|
||||
* company
|
||||
* notes
|
||||
* created_at
|
||||
* updated_at
|
||||
* deleted_at
|
||||
|
||||
---
|
||||
|
||||
# 7. Full REST Endpoint Specification
|
||||
|
||||
All endpoints return JSON.
|
||||
All errors return structured error format.
|
||||
|
||||
Error format:
|
||||
{
|
||||
"error": "string",
|
||||
"code": "string",
|
||||
"details": "optional"
|
||||
}
|
||||
|
||||
---
|
||||
|
||||
# AUTH
|
||||
|
||||
POST /auth/register
|
||||
POST /auth/login
|
||||
POST /auth/refresh
|
||||
POST /auth/logout
|
||||
GET /auth/me
|
||||
|
||||
---
|
||||
|
||||
# API KEYS
|
||||
|
||||
POST /api-keys
|
||||
GET /api-keys
|
||||
DELETE /api-keys/{id}
|
||||
|
||||
---
|
||||
|
||||
# USERS
|
||||
|
||||
GET /users/me
|
||||
PUT /users/me
|
||||
DELETE /users/me
|
||||
|
||||
---
|
||||
|
||||
# CALENDARS
|
||||
|
||||
GET /calendars
|
||||
POST /calendars
|
||||
GET /calendars/{id}
|
||||
PUT /calendars/{id}
|
||||
DELETE /calendars/{id}
|
||||
|
||||
Sharing
|
||||
POST /calendars/{id}/share
|
||||
GET /calendars/{id}/members
|
||||
DELETE /calendars/{id}/members/{user_id}
|
||||
|
||||
---
|
||||
|
||||
# EVENTS
|
||||
|
||||
GET /events
|
||||
GET /events/{id}
|
||||
POST /events
|
||||
PUT /events/{id}
|
||||
DELETE /events/{id}
|
||||
|
||||
Filters:
|
||||
GET /events?start=...&end=...
|
||||
GET /events?calendar_id=...
|
||||
GET /events?search=...
|
||||
GET /events?tag=...
|
||||
|
||||
---
|
||||
|
||||
# REMINDERS
|
||||
|
||||
POST /events/{id}/reminders
|
||||
DELETE /events/{id}/reminders/{id}
|
||||
|
||||
---
|
||||
|
||||
# ATTENDEES
|
||||
|
||||
POST /events/{id}/attendees
|
||||
PUT /events/{id}/attendees/{id}
|
||||
DELETE /events/{id}/attendees/{id}
|
||||
|
||||
---
|
||||
|
||||
# CONTACTS
|
||||
|
||||
GET /contacts
|
||||
POST /contacts
|
||||
GET /contacts/{id}
|
||||
PUT /contacts/{id}
|
||||
DELETE /contacts/{id}
|
||||
GET /contacts?search=...
|
||||
|
||||
---
|
||||
|
||||
# AVAILABILITY
|
||||
|
||||
GET /availability?calendar_id=...&start=...&end=...
|
||||
|
||||
---
|
||||
|
||||
# BOOKING
|
||||
|
||||
POST /calendars/{id}/booking-link
|
||||
GET /booking/{token}/availability
|
||||
POST /booking/{token}/reserve
|
||||
|
||||
---
|
||||
|
||||
# ICS
|
||||
|
||||
GET /calendars/{id}/export.ics
|
||||
POST /calendars/import
|
||||
|
||||
---
|
||||
|
||||
# 8. Index Requirements
|
||||
|
||||
Required indexes:
|
||||
|
||||
* events(calendar_id, start_time)
|
||||
* events(start_time)
|
||||
* calendars(owner_id)
|
||||
* contacts(owner_id)
|
||||
* api_keys(user_id)
|
||||
|
||||
---
|
||||
|
||||
# 9. Performance Rules
|
||||
|
||||
* All list endpoints paginated (limit + cursor)
|
||||
* No N+1 queries
|
||||
* All joins explicitly defined
|
||||
* Recurring events expanded lazily
|
||||
* Reminder queries indexed
|
||||
|
||||
---
|
||||
|
||||
# 10. Security Requirements
|
||||
|
||||
* Rate limiting middleware
|
||||
* UUID validation
|
||||
* Input validation
|
||||
* SQL injection safe queries
|
||||
* Password complexity enforcement
|
||||
* API key hashing
|
||||
|
||||
---
|
||||
|
||||
# 11. Development Phases
|
||||
|
||||
Phase 1
|
||||
|
||||
* Auth
|
||||
* Calendars
|
||||
* Events
|
||||
* Contacts
|
||||
|
||||
Phase 2
|
||||
|
||||
* Recurrence
|
||||
* Reminders
|
||||
* Sharing
|
||||
|
||||
Phase 3
|
||||
|
||||
* Booking links
|
||||
* Availability
|
||||
* ICS
|
||||
* WebSockets
|
||||
|
||||
---
|
||||
|
||||
This file defines the full system scope.
|
||||
|
||||
The next file (logic.md) will define detailed business rules, validation logic, permission flows, recurrence logic, reminder processing, and booking behavior.
|
||||
Reference in New Issue
Block a user