SapuJagat Backend

Lightweight documentation for the implemented foundation: health, country standards, qibla direction, Indonesia Kemenag locations, normalized prayer times, prayer marks, cache-backed prayer responses, Better Auth routing, and the social API foundation.

Version 0.1.11 Base URL: http://localhost:3000 Interactive Mockup: /mockup Architecture: docs/SapuJagat Backend Architecture.md

Changelog

v0.1.13

Native Apple Provider Registration

Allows native Apple sign-in to register with bundle-id-only backend configuration.

  • Registers the Apple provider when `APPLE_APP_BUNDLE_IDENTIFIER` is configured.
  • Keeps `APPLE_CLIENT_SECRET` optional for native ID-token sign-in and required only for web OAuth redirects.
  • Adds provider diagnostics that report registration status without exposing secrets.
v0.1.12

Native iOS Auth

Enabled native app session tokens for Better Auth social sign-in.

  • Added Better Auth bearer-token support for protected API routes.
  • Allowed comma-separated Google client IDs and Apple bundle ID audiences for native ID-token verification.
  • Documented the iOS ID-token exchange and Authorization header contract.
v0.1.11

Authenticated Prayer Marks

Added persistent per-user prayed-state sync for individual prayers.

  • Added the `user_prayer_marks` table keyed by user, date, and prayer name.
  • Exposed authenticated `GET`, `POST`, and `DELETE /v1/prayer-marks` routes.
  • Allows the app to restore marked-prayed history after reinstall when the user logs back in.
v0.1.10

Reverse Geocoded Nearby Inference

Improved `/v1/location/nearby` for Indonesia coordinates whose local bounding boxes are too narrow.

  • Added BigDataCloud reverse geocoding as a country-inference fallback.
  • Automatically enables Indonesia centroid recommendations when reverse geocoding returns `countryCode=ID`.
  • Boosts official locations matching the reverse-geocoded city before pure distance sorting.
v0.1.9

Qibla Direction Endpoint

Added coordinate-based qibla direction calculation to the normalized prayer API.

  • Exposed myQuran-style coordinate path input at `/v1/qibla/:coordinates`.
  • Added query-parameter input at `/v1/qibla` for app clients.
  • Returned latitude, longitude, and direction in degrees clockwise from true north.
v0.1.8

Social Safety Controls

Added block lists, mute lists, social settings, and the baseline Drizzle migration.

  • Blocks prevent friend requests and prayer nudges in either direction.
  • Mutes suppress APNs notification boundary responses.
  • Blocking removes reciprocal friendships and cancels pending requests.
v0.1.7

Social Flow Hardening

Closed friend-request and nudge lifecycle gaps after reviewing the v0.1.6 flow.

  • Friend requests now validate the addressee before insert.
  • Reverse pending friend requests are rejected explicitly.
  • Added cancel friend request and mark nudge read endpoints.
v0.1.6

Social Foundation API

Implemented the first authenticated social route surface.

  • Added device registration, friend requests, friendships, and prayer nudges.
  • Added Better Auth session protection for social routes.
  • Added notification queue boundary objects for social events.
v0.1.5

Coordinate-Based Official Location Recommendations

Changed nearby location resolution into ranked official location recommendations.

  • Added Indonesia centroid and bounding-box ranking.
  • Returned fallback coordinate location alongside recommendations.
  • Avoided accidental Indonesia recommendations for unsupported nearby countries.
v0.1.4

Lightweight API Documentation Landing Page

Added the backend documentation page served from `/` and `/docs`.

  • Documented implemented endpoints with example input and output.
  • Kept the page static and dependency-free.
  • Linked the page to the architecture document.
v0.1.3

Prayer Schedule Cache Layer

Added cache keys and Redis-backed storage for normalized prayer responses.

  • Cached day and month prayer responses.
  • Added in-memory fallback for local development.
  • Kept provider fallbacks compatible with cache reads.
v0.1.2

Code Hygiene and Package Modernization

Cleaned lint issues and modernized package usage.

  • Aligned TypeScript and ESLint behavior.
  • Cleaned location sync code.
  • Kept build and lint passing as a baseline.
v0.1.1

Indonesia Kemenag via myQuran v3

Added Indonesia official prayer-location support through myQuran v3.

  • Synced province and kab/kota catalog data.
  • Added Kemenag/myQuran provider adapter.
  • Exposed Indonesia province and kab/kota endpoints.
v0.1.0

Backend Foundation

Created the initial Hono, TypeScript, Drizzle, Better Auth, and module foundation.

  • Added health, auth, prayer, social, and notification module boundaries.
  • Defined normalized country, location, and prayer models.
  • Established the architecture document as the implementation ledger.

Implemented Endpoints

GET /health

Health Check

Confirms that the API process is running.

Example Input

GET /health

Example Output

{
  "status": "ok",
  "service": "sapujagat-backend",
  "timestamp": "2026-06-25T00:00:00.000Z"
}
GET /v1/country-standards

Country Standards

Lists normalized prayer country standards and their default providers.

Example Input

GET /v1/country-standards

Example Output

{
  "data": [
    {
      "countryCode": "ID",
      "countryName": "Indonesia",
      "locationMode": "city",
      "authorityName": "Kementerian Agama Republik Indonesia",
      "defaultProvider": "kemenag",
      "fallbackProvider": "calculation"
    }
  ]
}
GET /v1/qibla/:coordinates

Qibla Direction

Calculates the qibla bearing from latitude and longitude coordinates.

Example Input

GET /v1/qibla/-6.200000,106.816666

Example Output

{
  "data": {
    "latitude": -6.2,
    "longitude": 106.816666,
    "direction": 295.156312837391
  }
}
GET /v1/location/indonesia/provinces

Indonesia Provinces

Returns province choices used by the iOS location picker.

Example Input

GET /v1/location/indonesia/provinces

Example Output

{
  "data": [
    {
      "code": "ID-JB",
      "name": "JAWA BARAT",
      "timezone": "Asia/Jakarta"
    }
  ]
}
GET /v1/location/indonesia/kabkota

Indonesia Kab/Kota List

Searches the locally synced Kemenag/myQuran kabupaten and kota catalog.

Example Input

GET /v1/location/indonesia/kabkota?q=bekasi

Example Output

{
  "data": [
    {
      "id": "cedebb6e872f539bef8c3f919874e9d7",
      "countryCode": "ID",
      "locationMode": "city",
      "displayName": "KOTA BEKASI",
      "officialCode": "cedebb6e872f539bef8c3f919874e9d7",
      "provinceCode": "ID-JB",
      "provinceName": "JAWA BARAT",
      "locationType": "kota",
      "timezone": "Asia/Jakarta"
    }
  ]
}
GET /v1/location/indonesia/kabkota/:id

Indonesia Kab/Kota Detail

Looks up a single Indonesia prayer location by myQuran/Kemenag location id.

Example Input

GET /v1/location/indonesia/kabkota/cedebb6e872f539bef8c3f919874e9d7

Example Output

{
  "data": {
    "id": "cedebb6e872f539bef8c3f919874e9d7",
    "displayName": "KOTA BEKASI",
    "provinceCode": "ID-JB",
    "provinceName": "JAWA BARAT",
    "timezone": "Asia/Jakarta"
  }
}
GET /v1/location/search

Normalized Location Search

Searches official provider locations through the country standard abstraction.

Example Input

GET /v1/location/search?countryCode=ID&q=bekasi

Example Output

{
  "data": [
    {
      "id": "cedebb6e872f539bef8c3f919874e9d7",
      "countryCode": "ID",
      "locationMode": "city",
      "displayName": "KOTA BEKASI",
      "timezone": "Asia/Jakarta"
    }
  ]
}
GET /v1/location/nearby

Nearby Location Recommendations

Ranks official prayer locations near user coordinates using local containment, reverse-geocoded country inference, and centroid distance.

Example Input

GET /v1/location/nearby?latitude=-6.2383&longitude=106.9756&limit=10

Example Output

{
  "data": {
    "input": {
      "latitude": -6.2383,
      "longitude": 106.9756
    },
    "inferredCountryCode": "ID",
    "locationMode": "city",
    "recommendations": [
      {
        "rank": 1,
        "distanceKm": 2.13,
        "confidence": "high",
        "location": {
          "id": "cedebb6e872f539bef8c3f919874e9d7",
          "countryCode": "ID",
          "locationMode": "city",
          "displayName": "KOTA BEKASI",
          "officialCode": "cedebb6e872f539bef8c3f919874e9d7",
          "latitude": -6.2349858,
          "longitude": 106.9945444,
          "timezone": "Asia/Jakarta",
          "metadata": {
            "provinceCode": "ID-JB",
            "provinceName": "JAWA BARAT",
            "locationType": "kota",
            "source": "myQuran v3 / Kemenag Bimas Islam"
          }
        }
      }
    ],
    "fallbackLocation": {
      "id": "coord:-6.23830,106.97560",
      "countryCode": "XX",
      "locationMode": "coordinate",
      "displayName": "Coordinate Location",
      "latitude": -6.2383,
      "longitude": 106.9756,
      "timezone": "UTC"
    }
  }
}
GET /v1/prayer-times/day

Prayer Times Day

Returns one normalized prayer day. Indonesia requests use Kemenag data through myQuran v3 and the cache layer.

Example Input

GET /v1/prayer-times/day?countryCode=ID&location=KOTA%20BEKASI&date=2026-06-24

Example Output

{
  "data": {
    "date": "2026-06-24",
    "timezone": "Asia/Jakarta",
    "location": {
      "id": "cedebb6e872f539bef8c3f919874e9d7",
      "countryCode": "ID",
      "locationMode": "city",
      "displayName": "KOTA BEKASI",
      "timezone": "Asia/Jakarta"
    },
    "source": {
      "providerCode": "kemenag",
      "countryCode": "ID",
      "authorityName": "Kementerian Agama Republik Indonesia",
      "methodName": "Kemenag Bimas Islam via myQuran v3"
    },
    "times": {
      "fajr": "04:40",
      "sunrise": "05:58",
      "dhuhr": "11:58",
      "asr": "15:19",
      "maghrib": "17:50",
      "isha": "19:05"
    }
  }
}
GET /v1/prayer-times/month

Prayer Times Month

Returns normalized prayer days for a full month.

Example Input

GET /v1/prayer-times/month?countryCode=ID&location=KOTA%20BEKASI&year=2026&month=6

Example Output

{
  "data": {
    "year": 2026,
    "month": 6,
    "timezone": "Asia/Jakarta",
    "location": {
      "id": "cedebb6e872f539bef8c3f919874e9d7",
      "displayName": "KOTA BEKASI"
    },
    "days": [
      {
        "date": "2026-06-01",
        "times": {
          "fajr": "04:36",
          "sunrise": "05:53",
          "dhuhr": "11:53",
          "asr": "15:14",
          "maghrib": "17:47",
          "isha": "19:00"
        }
      }
    ]
  }
}
GET /v1/prayer-marks

Prayer Mark List

Lists the authenticated user's prayers marked as prayed, optionally filtered by local prayer date range.

Example Input

GET /v1/prayer-marks?from=2026-07-01&to=2026-07-31
Authenticated session headers

Example Output

{
  "data": [
    {
      "userId": "user_123",
      "prayerDate": "2026-07-01",
      "prayerName": "fajr",
      "createdAt": "2026-07-01T21:30:00.000Z",
      "updatedAt": "2026-07-01T21:30:00.000Z"
    }
  ]
}
POST /v1/prayer-marks

Mark Prayer Prayed

Creates or updates one prayed-state row for the authenticated user.

Example Input

POST /v1/prayer-marks
Authenticated session headers

{"prayerDate":"2026-07-01","prayerName":"fajr"}

Example Output

{
  "data": {
    "userId": "user_123",
    "prayerDate": "2026-07-01",
    "prayerName": "fajr"
  }
}
DELETE /v1/prayer-marks/:prayerDate/:prayerName

Unmark Prayer Prayed

Deletes one prayed-state row owned by the authenticated user.

Example Input

DELETE /v1/prayer-marks/2026-07-01/fajr
Authenticated session headers

Example Output

{
  "data": {
    "userId": "user_123",
    "prayerDate": "2026-07-01",
    "prayerName": "fajr"
  }
}
GET /v1/social/settings

Social Settings

Returns authenticated user social preferences with default-open behavior when no row exists.

Example Input

GET /v1/social/settings
Authenticated session headers

Example Output

{
  "data": {
    "userId": "user_123",
    "allowFriendRequests": true,
    "allowPrayerNudges": true
  }
}
PATCH /v1/social/settings

Update Social Settings

Updates friend-request and prayer-nudge receiving preferences for the authenticated user.

Example Input

PATCH /v1/social/settings
Authenticated session headers

{"allowPrayerNudges":false}

Example Output

{
  "data": {
    "userId": "user_123",
    "allowFriendRequests": true,
    "allowPrayerNudges": false
  }
}
POST /v1/social/devices

Device Registration

Registers or refreshes an authenticated user's push-capable device token.

Example Input

POST /v1/social/devices
Authenticated session headers

{"platform":"ios","pushToken":"apns-token-value","deviceName":"Dandy iPhone"}

Example Output

{
  "data": {
    "id": "a7b5b6c8-6c7d-4b3a-9f7a-7b94a9f8f0f1",
    "userId": "user_123",
    "platform": "ios",
    "pushToken": "apns-token-value",
    "deviceName": "Dandy iPhone"
  }
}
POST /v1/social/friend-requests

Create Friend Request

Creates a pending friend request and queues a friend-request notification event.

Example Input

POST /v1/social/friend-requests
Authenticated session headers

{"addresseeId":"user_456"}

Example Output

{
  "data": {
    "request": {
      "id": "f0c01b16-f836-4a90-a00f-fd76a909c4dd",
      "requesterId": "user_123",
      "addresseeId": "user_456",
      "status": "pending"
    },
    "notification": {
      "channel": "apns",
      "event": "friend_request",
      "status": "queued"
    }
  }
}
GET /v1/social/friend-requests

Friend Request List

Lists incoming and outgoing friend requests for the authenticated user.

Example Input

GET /v1/social/friend-requests
Authenticated session headers

Example Output

{
  "data": [
    {
      "id": "f0c01b16-f836-4a90-a00f-fd76a909c4dd",
      "requesterId": "user_123",
      "addresseeId": "user_456",
      "status": "pending"
    }
  ]
}
POST /v1/social/friend-requests/:id/accept

Accept Friend Request

Accepts a pending friend request addressed to the authenticated user and creates reciprocal friendship rows.

Example Input

POST /v1/social/friend-requests/f0c01b16-f836-4a90-a00f-fd76a909c4dd/accept
Authenticated session headers

Example Output

{
  "data": {
    "id": "f0c01b16-f836-4a90-a00f-fd76a909c4dd",
    "requesterId": "user_123",
    "addresseeId": "user_456",
    "status": "accepted"
  }
}
POST /v1/social/friend-requests/:id/decline

Decline Friend Request

Declines a pending friend request addressed to the authenticated user.

Example Input

POST /v1/social/friend-requests/f0c01b16-f836-4a90-a00f-fd76a909c4dd/decline
Authenticated session headers

Example Output

{
  "data": {
    "id": "f0c01b16-f836-4a90-a00f-fd76a909c4dd",
    "requesterId": "user_123",
    "addresseeId": "user_456",
    "status": "declined"
  }
}
POST /v1/social/friend-requests/:id/cancel

Cancel Friend Request

Cancels a pending outgoing friend request owned by the authenticated user.

Example Input

POST /v1/social/friend-requests/f0c01b16-f836-4a90-a00f-fd76a909c4dd/cancel
Authenticated session headers

Example Output

{
  "data": {
    "id": "f0c01b16-f836-4a90-a00f-fd76a909c4dd",
    "requesterId": "user_123",
    "addresseeId": "user_456",
    "status": "cancelled"
  }
}
GET /v1/social/friends

Friend List

Lists accepted friendship rows for the authenticated user.

Example Input

GET /v1/social/friends
Authenticated session headers

Example Output

{
  "data": [
    {
      "userId": "user_456",
      "friendId": "user_123",
      "createdAt": "2026-06-27T00:00:00.000Z"
    }
  ]
}
POST /v1/social/blocks/:userId

Block User

Blocks another user from social interactions with the authenticated user.

Example Input

POST /v1/social/blocks/user_456
Authenticated session headers

Example Output

{
  "data": {
    "userId": "user_123",
    "blockedUserId": "user_456"
  }
}
DELETE /v1/social/blocks/:userId

Unblock User

Removes an authenticated user's block for another user.

Example Input

DELETE /v1/social/blocks/user_456
Authenticated session headers

Example Output

{
  "data": {
    "userId": "user_123",
    "blockedUserId": "user_456"
  }
}
GET /v1/social/blocks

Blocked User List

Lists users blocked by the authenticated user.

Example Input

GET /v1/social/blocks
Authenticated session headers

Example Output

{
  "data": [
    {
      "userId": "user_123",
      "blockedUserId": "user_456"
    }
  ]
}
POST /v1/social/mutes/:userId

Mute User

Suppresses notification fan-out for social actions from another user.

Example Input

POST /v1/social/mutes/user_456
Authenticated session headers

Example Output

{
  "data": {
    "userId": "user_123",
    "mutedUserId": "user_456"
  }
}
DELETE /v1/social/mutes/:userId

Unmute User

Removes an authenticated user's mute for another user.

Example Input

DELETE /v1/social/mutes/user_456
Authenticated session headers

Example Output

{
  "data": {
    "userId": "user_123",
    "mutedUserId": "user_456"
  }
}
GET /v1/social/mutes

Muted User List

Lists users muted by the authenticated user.

Example Input

GET /v1/social/mutes
Authenticated session headers

Example Output

{
  "data": [
    {
      "userId": "user_123",
      "mutedUserId": "user_456"
    }
  ]
}
GET /v1/social/nudges

Prayer Nudge List

Lists sent and received prayer nudges for the authenticated user.

Example Input

GET /v1/social/nudges
Authenticated session headers

Example Output

{
  "data": [
    {
      "id": "df3d1a9d-5207-45c2-8df7-1b53866819a7",
      "senderId": "user_123",
      "recipientId": "user_456",
      "prayerName": "fajr",
      "message": "Subuh yuk",
      "status": "queued"
    }
  ]
}
POST /v1/social/nudges

Send Prayer Nudge

Stores a prayer nudge for an accepted friend and queues the notification boundary event.

Example Input

POST /v1/social/nudges
Authenticated session headers

{"recipientId":"user_456","prayerName":"fajr","message":"Subuh yuk"}

Example Output

{
  "data": {
    "nudge": {
      "id": "df3d1a9d-5207-45c2-8df7-1b53866819a7",
      "senderId": "user_123",
      "recipientId": "user_456",
      "prayerName": "fajr",
      "message": "Subuh yuk",
      "status": "queued"
    },
    "notification": {
      "channel": "apns",
      "event": "prayer_nudge",
      "status": "queued"
    }
  }
}
POST /v1/social/nudges/:id/read

Mark Prayer Nudge Read

Marks a received prayer nudge as read for the authenticated recipient.

Example Input

POST /v1/social/nudges/df3d1a9d-5207-45c2-8df7-1b53866819a7/read
Authenticated session headers

Example Output

{
  "data": {
    "id": "df3d1a9d-5207-45c2-8df7-1b53866819a7",
    "senderId": "user_123",
    "recipientId": "user_456",
    "prayerName": "fajr",
    "status": "read"
  }
}
POST /api/auth/sign-in/social

Native Social Sign-In

Exchanges an Apple or Google provider ID token for a Better Auth session token. iOS should store the returned token in Keychain.

Example Input

POST /api/auth/sign-in/social

{"provider":"apple","idToken":{"token":"apple-identity-token","nonce":"nonce-used-for-apple-request"}}

Example Output

{
  "redirect": false,
  "token": "better-auth-session-token",
  "user": {
    "id": "user_123",
    "email": "dandy@example.com",
    "name": "Dandy Candrasaputra"
  }
}
GET /api/auth/get-session

Get Auth Session

Returns the current Better Auth session. Native clients can authenticate this request with Authorization: Bearer <session-token>.

Example Input

GET /api/auth/get-session
Authorization: Bearer better-auth-session-token

Example Output

{
  "session": {
    "id": "session_123",
    "userId": "user_123",
    "expiresAt": "2026-07-14T00:00:00.000Z"
  },
  "user": {
    "id": "user_123",
    "email": "dandy@example.com",
    "name": "Dandy Candrasaputra"
  }
}