/ docs / api-design.md
api-design.md
  1  # crash api design
  2  
  3  **base url:** `https://crash.app/api/v1`
  4  
  5  **authentication:** magic link tokens in http-only cookies or authorization header
  6  
  7  ---
  8  
  9  ## endpoints
 10  
 11  ### **authentication**
 12  
 13  **POST /auth/request-magic-link**
 14  - request: `{ "email": "user@example.com" }`
 15  - response: `{ "message": "magic link sent to email" }`
 16  - sends email with unique token link
 17  - rate limit: 3 requests per email per hour
 18  
 19  **GET /auth/verify?token={token}**
 20  - validates magic link token
 21  - sets auth cookie or returns jwt
 22  - redirects to pwa (or returns token for spa)
 23  - response: `{ "token": "jwt...", "user": {...} }`
 24  
 25  **POST /auth/logout**
 26  - clears auth cookie
 27  - response: `{ "message": "logged out" }`
 28  
 29  **GET /auth/me**
 30  - returns current user info
 31  - response: `{ "user": {...} }`
 32  
 33  ---
 34  
 35  ### **users**
 36  
 37  **POST /users** (signup)
 38  - request:
 39  ```json
 40  {
 41    "email": "user@example.com",
 42    "name": "alex",
 43    "city": "berlin",
 44    "pronouns": "they/them",
 45    "birth_year": 1995,
 46    "bio": "touring musician, love cats",
 47    "telegram_chat_id": "123456789",
 48    "contact_info": "@alex_pdx on ig/tg",
 49    "invited_by_id": "uuid-of-inviter"
 50  }
 51  ```
 52  - response: `{ "user": {...}, "message": "account created, check email for magic link" }`
 53  - sends magic link email automatically
 54  
 55  **GET /users/me**
 56  - returns current user's full profile
 57  - response: `{ "user": {...} }`
 58  
 59  **PATCH /users/me**
 60  - update current user's profile
 61  - request: partial user object (any editable fields)
 62  - response: `{ "user": {...} }`
 63  
 64  **GET /users/search?city={city}&query={name}**
 65  - search users for "invited by" dropdown
 66  - filters by city, searches name/contact_info
 67  - response: `{ "users": [{id, name, pronouns, birth_year, city, contact_info}] }`
 68  - limit: 20 results
 69  
 70  ---
 71  
 72  ### **housing posts** (seeking only for mvp)
 73  
 74  **POST /posts**
 75  - create new seeking post
 76  - request:
 77  ```json
 78  {
 79    "city": "berlin",
 80    "dates_start": "2025-10-20",
 81    "dates_end": "2025-10-22",
 82    "urgency": "emergency",
 83    "notification_text": "need couch in berlin, band tour fell through 😭",
 84    "description": "hi! our airbnb cancelled last minute. we're a 3-piece punk band touring europe. need floor space for 2 nights. we're quiet, respectful, can help with dishes/cooking. have gear so need some storage space too."
 85  }
 86  ```
 87  - response: `{ "post": {...} }`
 88  - triggers notifications (see notification flows below)
 89  
 90  **GET /posts**
 91  - list active posts (for "recent requests" feed)
 92  - query params:
 93    - `city` (optional filter)
 94    - `status` (default: active)
 95    - `limit` (default: 20)
 96    - `offset` (for pagination)
 97  - response: `{ "posts": [...], "total": 45 }`
 98  
 99  **GET /posts/{id}**
100  - get single post with responses
101  - response:
102  ```json
103  {
104    "post": {
105      "id": "...",
106      "user": {
107        "name": "alex",
108        "pronouns": "they/them",
109        "city": "berlin",
110        "telegram_chat_id": "123456789",
111        "contact_info": "@alex_pdx on ig",
112        "invited_by": {
113          "name": "maya",
114          "city": "berlin"
115        }
116      },
117      "city": "berlin",
118      "dates_start": "2025-10-20",
119      "dates_end": "2025-10-22",
120      "urgency": "emergency",
121      "notification_text": "need couch in berlin, band tour fell through 😭",
122      "description": "...",
123      "status": "active",
124      "created_at": "...",
125      "responses": [
126        {
127          "id": "...",
128          "responder": {
129            "name": "jordan",
130            "pronouns": "she/her",
131            "city": "berlin",
132            "telegram_chat_id": "987654321",
133            "contact_info": "@jordan_berlin on tg",
134            "invited_by": {
135              "name": "alex",
136              "city": "berlin"
137            }
138          },
139          "notes": "i have a couch, cats, quiet hours after 10pm",
140          "status": "pending",
141          "created_at": "..."
142        }
143      ]
144    }
145  }
146  ```
147  - only post owner sees full responses list
148  - responders can see their own response
149  
150  **PATCH /posts/{id}**
151  - update post (owner only)
152  - can update: description, status
153  - request: `{ "status": "fulfilled" }`
154  - response: `{ "post": {...} }`
155  
156  ---
157  
158  ### **responses**
159  
160  **POST /posts/{post_id}/responses**
161  - respond to a seeking post
162  - request:
163  ```json
164  {
165    "notes": "i have a couch available those dates. i have 2 cats, quiet hours after 10pm. lmk!"
166  }
167  ```
168  - response: `{ "response": {...} }`
169  - sends email notification to post owner
170  
171  **PATCH /responses/{id}**
172  - update response status (post owner or responder)
173  - request: `{ "status": "accepted" }` or `{ "status": "completed" }`
174  - response: `{ "response": {...} }`
175  - when status -> "completed": increments responder's housing_provided_count (tracked but not displayed in mvp)
176  
177  **GET /responses/mine**
178  - get all responses current user has made
179  - response: `{ "responses": [...] }`
180  
181  ---
182  
183  ### **notification preferences**
184  
185  **GET /preferences**
186  - get current user's notification preferences
187  - response: `{ "preferences": {...} }`
188  
189  **PATCH /preferences**
190  - update notification preferences
191  - request:
192  ```json
193  {
194    "email_enabled": true,
195    "telegram_enabled": true,
196    "push_enabled": true,
197    "emergency_only": false,
198    "can_offer_housing": true
199  }
200  ```
201  - response: `{ "preferences": {...} }`
202  - note: `can_offer_housing` is technically on users table but exposed here for ux simplicity
203  
204  ---
205  
206  ## notification flows
207  
208  ### **when post created (POST /posts)**
209  
210  **email to seeker (post creator):**
211  ```
212  subject: ✅ your housing request is live
213  
214  hey [name],
215  
216  your housing request for [city] ([dates]) is now live!
217  
218  we've notified [count] people in [city] who can offer housing.
219  
220  view your post and responses: [link to post]
221  
222  you'll get an email each time someone responds.
223  
224  - crash
225  ```
226  
227  **email to all potential offerers:**
228  - recipients: all users where `can_offer_housing=true AND city=[post.city]`
229  - subject: `🏠 [notification_text]`
230  - body:
231  ```
232  hey [name],
233  
234  someone in your city needs housing:
235  
236  📍 [city]
237  📅 [dates_start] - [dates_end]
238  ⚡ urgency: [urgency]
239  
240  [notification_text]
241  
242  ---
243  [description]
244  ---
245  
246  about them:
247  [seeker name] ([pronouns]) - invited by [inviter name]
248  contact: telegram | [contact_info]
249  
250  respond: [link to post]
251  
252  update your notification preferences: [link]
253  
254  - crash
255  ```
256  
257  ### **when response created (POST /posts/{id}/responses)**
258  
259  **email to seeker (post owner):**
260  ```
261  subject: 🎉 someone responded to your housing request
262  
263  hey [name],
264  
265  good news! [responder name] can help with your request in [city].
266  
267  their response:
268  "[notes]"
269  
270  contact them:
271  telegram: [link to direct message responder]
272  other: [responder contact_info]
273  
274  view all responses: [link to post]
275  
276  - crash
277  ```
278  
279  ### **when post expires (automatic, dates_end passes)**
280  
281  **email to seeker:**
282  ```
283  subject: your housing request has expired
284  
285  hey [name],
286  
287  your housing request for [city] ([dates]) has expired.
288  
289  if you still need housing, you can create a new request: [link]
290  
291  - crash
292  ```
293  
294  ---
295  
296  ## status codes
297  
298  - 200: success
299  - 201: created
300  - 400: bad request (validation error)
301  - 401: unauthorized (not logged in)
302  - 403: forbidden (not allowed to access this resource)
303  - 404: not found
304  - 429: rate limited
305  - 500: server error
306  
307  ---
308  
309  ## error response format
310  
311  ```json
312  {
313    "error": "validation_error",
314    "message": "city is required",
315    "details": {
316      "field": "city",
317      "issue": "required field missing"
318    }
319  }
320  ```
321  
322  ---
323  
324  ## implementation notes
325  
326  ### **authentication**
327  - use jwt with http-only cookies (secure, no localstorage xss risk)
328  - magic links expire after 15 minutes
329  - tokens should be single-use (delete after verification)
330  
331  ### **authorization**
332  - middleware to check auth on protected routes
333  - users can only edit their own posts/profile
334  - post owners can see all responses, responders see only their own
335  
336  ### **post expiration**
337  - check dates_end on GET requests
338  - if `dates_end < today AND status='active'`, update to 'expired'
339  - send email notification to post owner when expired
340  
341  ### **notifications**
342  - use a proper email service (sendgrid, postmark, etc)
343  - all emails should have unsubscribe link
344  - respect user's notification preferences
345  - emergency posts bypass "emergency_only" filter
346  
347  ### **rate limiting**
348  - magic link requests: 3 per email per hour
349  - post creation: 5 per user per day (prevent spam)
350  - response creation: 10 per user per hour
351  
352  ### **validation**
353  - dates_start must be <= dates_end
354  - dates must be in the future (or today for emergency)
355  - city normalization (lowercase, trim whitespace)
356  - email validation (proper format)
357  - notification_text max 150 chars
358  - telegram_chat_id format validation (numeric string)
359  
360  ### **privacy**
361  - email addresses are never exposed via api
362  - only show user fields explicitly listed in response schemas
363  - telegram contact and contact_info are shown to facilitate connection
364  
365  ### **database queries to optimize**
366  - finding offerers for notifications: `SELECT * FROM users WHERE can_offer_housing=true AND city=$1`
367  - listing active posts: `SELECT * FROM housing_posts WHERE status='active' ORDER BY created_at DESC`
368  - checking for expired posts: done on read, not scheduled job (simpler for mvp)
369  
370  ---
371  
372  ## future considerations (post-mvp)
373  
374  - offering posts (users can list standing availability)
375  - full vouching system with trust chain visualization
376  - response messages/threads (in-app chat before telegram handoff)
377  - image uploads (housing space photos)
378  - push notifications for pwa
379  - geographic radius search (not just exact city match)
380  - multi-city posts ("berlin OR hamburg")
381  - calendar integration (ics export)
382  - moderation/reporting system