A Spring Boot backend for a Hadith encyclopedia serving web and mobile applications.
A fourth-year university team project that brings Hadith texts, sources, narrators, scholarly rulings, explanations, and user interactions into one connected platform.
This repository contains the backend application, its database migrations, automated tests, and development configuration.
Backend development: Jamil Helal
Overview · My contribution · Architecture · Database · Demo · Technical decisions · Limitations
Tech stack · Local setup · Tests · API reference · Migrations
Branch scope
The setup instructions and database diagrams in this README describe
main.Semantic and hybrid search have a separate implementation on
feat/semantic. These modes require that branch's embedding service, database migration, and configuration. They are not included in themainruntime documented here.
A Hadith text shared without its source, narrator, scholarly ruling, or explanation loses important context. Users also need a clear way to distinguish documented material from widely circulated texts that are not authentic.
Ahadith addresses this information-organization problem through a searchable encyclopedia with connected reference data and role-based content management.
The application stores and presents documented information. It does not independently determine the religious authenticity of a Hadith.
The project consists of three connected components:
| Component | Responsibility |
|---|---|
| React web application | Browser-based access to the encyclopedia and administration interfaces. |
| Flutter mobile application | Mobile access to content, account features, and notifications. |
| Spring Boot backend | Shared API, authentication, authorization, business rules, database access, and external integrations. |
Both clients use the same backend and database, keeping account behavior, permissions, and content operations consistent.
| Area | Capabilities |
|---|---|
| Hadith catalog | Hadith records, books, narrators, muhaddiths, rulings, explanations, topics, and related records. |
| Search | Normalized phrase matching, PostgreSQL full-text search, filtering, pagination, and authenticated search history. |
| Authentication | Email/password login, email verification, password reset, Google ID token login, JWT access tokens, and refresh sessions. |
| User features | Profile management, password changes, favorites, comments, and questions. |
| Scholar workflows | Scholar-upgrade requests, document submission, and administrative review. |
| Administration | Content management, user search, account activation/deactivation, and role management. |
| Notifications | Firebase Cloud Messaging integration, including notifications for newly added non-authentic circulated texts. |
| Operations | Database migrations, activity logging, scheduled security-data cleanup, health endpoints, and test configuration. |
The separate feat/semantic branch extends the search system with:
SEMANTIC: BGE-M3 embeddings and pgvector cosine similarity.HYBRID: full-text and semantic candidate rankings combined through Reciprocal Rank Fusion.- Administrative embedding backfill and status endpoints.
- Text-search fallback in hybrid mode when the embedding service is unavailable.
See the semantic search documentation for setup and branch-specific behavior.
I am Jamil Helal, responsible for backend development in this team project.
My work focuses on the Spring Boot API and the server-side behavior shared by the web and mobile applications:
- Designing and implementing API contracts and business logic.
- Working on relational database integration and versioned migrations.
- Implementing authentication, authorization, and account workflows.
- Supporting catalog management, search, and administrative operations.
- Integrating backend services with the web and mobile clients.
- Maintaining backend configuration, tests, and deployment-related setup.
The React and Flutter applications are separate team deliverables. This description identifies my backend responsibility without claiming sole authorship of the complete platform.
| Area | Location |
|---|---|
| Application source | src/main/java/com/jamil/ahadith |
| Feature modules | src/main/java/com/jamil/ahadith/features |
| Shared infrastructure | src/main/java/com/jamil/ahadith/core |
| Database migrations | src/main/resources/db/migration |
| Tests | src/test |
| Application configuration | src/main/resources/application.yml |
| Component | Repository |
|---|---|
| Spring Boot backend | Ahadith-spring |
| React web application | ahadithReact |
| Flutter mobile application | Hadith mobile application |
The core backend is a single Spring Boot application organized by feature, with shared security, configuration, and infrastructure code.
Controllers handle HTTP requests, services implement business rules, and repositories manage database operations. Request and response DTOs separate the public API contract from persistence entities.
flowchart LR
Web["React web application"]
Mobile["Flutter mobile application"]
subgraph Backend["Spring Boot backend"]
Security["Security filters and role checks"]
Controllers["REST controllers and validation"]
Services["Application services and business rules"]
Repositories["Repositories and database queries"]
Security --> Controllers
Controllers --> Services
Services --> Repositories
end
Database[("PostgreSQL / Neon")]
Google["Google ID token verification"]
Email["Resend email"]
Media["Cloudinary images and private PDFs"]
Push["Firebase Cloud Messaging"]
Web -->|HTTPS / JSON| Security
Mobile -->|HTTPS / JSON| Security
Repositories --> Database
Services --> Google
Services --> Email
Services --> Media
Services --> Push
Push -->|Push notification| Mobile
- A web or mobile client sends a request to the API.
- Security filters determine whether the route is public or requires an authenticated user.
- Authorization rules check the required role.
- The controller validates the request and passes it to the appropriate service.
- The service applies business rules and performs database operations through repositories.
- The backend returns a response DTO or a consistent error response.
When a non-authentic circulated text is created, the associated FCM listener runs after the database transaction commits.
This prevents the application from sending a creation notification for a record that was rolled back.
The notification payload contains fakeHadithId, allowing a client to navigate to the corresponding detail screen.
Delivery requires configured Firebase credentials, valid device tokens, and client-side notification handling.
Implementation:
The project includes configuration for a hosted PostgreSQL database on Neon and documents Render-specific proxy settings.
For local development, the application can connect to Neon using environment configuration. Docker Compose also provides an explicitly selected local PostgreSQL stack.
These are different database paths. Starting a local container should not be confused with connecting to the hosted database.
The semantic-search branch adds a Python embedding service and a separate pgvector-backed table. Those components are not included in the main architecture diagram above.
The schema separates reference content, user interactions, and account/security records.
The diagrams below are simplified views of main. They intentionally omit some columns and relationships to keep the main data model readable.
The Flyway migrations remain the authoritative schema definition.
erDiagram
muhaddiths |o..o{ books : referenced_by
books |o..o{ ahadith : contains
rawis |o..o{ ahadith : narrates
ruling |o..o{ ahadith : classifies
explaining |o..o{ ahadith : explains
ahadith ||..o{ topic_classes : classified_through
topics ||..o{ topic_classes : assigned_through
ahadith |o..o{ fake_ahadith : alternative_for
ruling |o..o{ fake_ahadith : classifies
books {
uuid id PK
text name
uuid muhaddith FK
}
ahadith {
uuid id PK
text text
int hadith_number
uuid book FK
uuid rawi FK
uuid ruling FK
uuid explaining FK
uuid sub_valid FK
}
topic_classes {
uuid id PK
uuid topic FK
uuid hadith FK
}
fake_ahadith {
uuid id PK
text text
uuid sub_valid FK
uuid ruling FK
}
The optional parent markers reflect nullable foreign keys in the actual schema. For example, a Hadith record can exist without a linked book, narrator, ruling, or explanation.
fake_ahadith.sub_valid optionally points to an alternative record in ahadith; it does not represent a one-to-one relationship.
erDiagram
users ||..o{ favorites : saves
ahadith ||..o{ favorites : saved_as
users ||..o{ questions : asks
ahadith |o..o{ questions : referenced_by
users ||..o{ upgrade_requests : submits
users ||..o{ refresh_token_sessions : owns
users ||..o{ user_fcm_tokens : registers
users {
uuid id PK
text email UK
user_type type
user_status status
}
favorites {
uuid id PK
uuid user_id FK
uuid hadith FK
}
questions {
uuid id PK
uuid asker FK
uuid hadith_id FK
text asker_text
text answer_text
}
upgrade_requests {
uuid id PK
uuid user_id FK
uuid reviewed_by FK
pro_upgrade_status status
}
The reviewer relationship, audit fields, and supporting account-token tables are omitted from this diagram for readability.
| Tables | Responsibility |
|---|---|
ahadith, books, rawis, muhaddiths, ruling, explaining |
Hadith content and its bibliographic, narrator, ruling, and explanatory records. |
topics, topic_classes |
Topic definitions and many-to-many Hadith classification. |
fake_ahadith, similar_ahadith |
Non-authentic circulated texts and explicit links between Hadith records. |
users, favorites, comments, questions, search_history |
Accounts and user interactions. |
upgrade_requests |
Scholar-upgrade submissions, document metadata, and review decisions. |
refresh_token_sessions, email_verification_tokens, password_reset_tokens, login_attempts |
Session lifecycle, account verification/recovery, and login-attempt tracking. |
notifications, user_fcm_tokens |
Stored notification records and registered device tokens. |
activity_log |
Actor information, affected records, and change data for audit use. |
Hadith numbering
The schema requires a positive Hadith number and defines a unique (book, hadith_number) pair.
Because book is nullable, this composite constraint is not a global uniqueness guarantee for records without a book.
Duplicate relationships
Unique pair constraints prevent duplicate favorites and duplicate assignments of the same topic to the same Hadith.
Scholar-upgrade requests
A partial unique index permits only one open upgrade request per user while retaining completed request history.
FCM token ownership
Migration V11 adds global FCM token uniqueness, rather than relying only on a (user_id, fcm_token) pair.
Deletion behavior
Many catalog relationships use ON DELETE SET NULL, preserving the referencing record when its related catalog record is deleted.
Dependent interaction records, such as favorites, use cascading deletion where defined in the schema.
Search representations
ahadith.search_vector is a PostgreSQL tsvector used for full-text search.
It is not an AI embedding.
Likewise, similar_ahadith stores explicit links between records rather than a semantic vector index. The semantic branch introduces a separate hadith_embeddings table.
This is the web address configured for the project. Availability and supported features depend on the deployed frontend and backend versions.
- Browse a book and open a Hadith.
- Inspect its linked source, narrator, ruling, and explanation.
- Search for a phrase and apply a catalog filter.
- Sign in with an authorized member account to explore favorites and questions.
- Use an authorized administrator account to demonstrate content management or scholar-upgrade review.
Demonstrate semantic and hybrid search only on a deployment that includes the semantic implementation, its embedding service, and populated embeddings.
Do not publish real administrator credentials or access tokens in the repository.
A curated screenshot gallery and recorded walkthrough have not been added to this README yet.
Recommended captures include the catalog, search results, Hadith details, and administrative workflows.
The following decisions describe choices visible in the implementation and their trade-offs.
Problem: Both clients need consistent authentication, permissions, and business rules.
Choice: A shared Spring Boot API with server-side validation and authorization.
Trade-off: Clients avoid duplicating core rules, but both depend on the availability and compatibility of the same backend.
Problem: Hadith records reference reusable books, narrators, rulings, explanations, and topics.
Choice: PostgreSQL tables, foreign keys, join tables, and Flyway migrations.
Trade-off: Relationships and integrity rules are explicit, but schema changes must remain compatible across development and deployment environments.
Problem: Arabic text needs search preparation without replacing the original displayed content.
Choice: Separate original/search fields, PostgreSQL full-text search, and trigram indexes.
Trade-off: Search preparation stays separate from the source text. Search quality still depends on normalization, query behavior, and the quality of the underlying data.
Problem: A user session may need to become invalid before an access token naturally expires.
Choice: JWT access tokens, hashed refresh-session records, refresh-token rotation, and a user tokenVersion.
Trade-off: Sensitive account changes can invalidate existing sessions, while the application retains server-side session state and associated lifecycle management.
Problem: Scholar-upgrade documents should not become publicly accessible uploads.
Choice: Backend PDF validation, authenticated Cloudinary storage, and short-lived document links returned through authorized endpoints.
Trade-off: Access is controlled before link creation, while provider configuration and document-link lifetime remain operational responsibilities.
Problem: A notification should not announce a record that failed to persist.
Choice: Send the creation notification through an after-commit transaction listener.
Trade-off: The record exists before sending, but the listener itself is not a durable retry queue.
The semantic branch separates model inference into a Python embedding service and stores vectors in PostgreSQL using pgvector.
Its hybrid search combines text and semantic candidate rankings through Reciprocal Rank Fusion, with text-search fallback when the embedding service is unavailable.
This introduces another runtime dependency and requires embedding generation, model configuration, and ongoing index maintenance.
See the feature-branch search documentation.
| Area | Current boundary |
|---|---|
| Content quality | Bundled seed data is for development and demonstration, not a final or authoritative religious reference. Production content requires reviewed sourcing and validation. |
| Branch-specific search | Semantic and hybrid search are implemented on feat/semantic, not in the main runtime documented here. |
| Notification delivery | The after-commit listener logs sending failures but does not itself persist a durable retry job. |
| Question answers | The current questions table stores one answer_text field rather than a separate collection of answers from multiple scholars. |
| External integrations | Google login, email, media storage, and push notifications depend on valid provider configuration. |
| API compatibility | Some legacy route aliases remain temporarily. New clients should use /api/v1. |
| Presentation and evaluation | A curated screenshot gallery and walkthrough video remain documentation improvements. This README does not claim benchmark results or a measured test-coverage percentage. |
| Area | Technology |
|---|---|
| Language | Java 21 |
| Backend framework | Spring Boot 4.1.0 |
| Security | Spring Security and JWT |
| Persistence | Spring Data JPA / Hibernate |
| Database | PostgreSQL 16 |
| Database migrations | Flyway |
| Build tooling | Maven Wrapper |
| Media storage | Cloudinary |
| Resend | |
| Push notifications | Firebase Cloud Messaging |
| Containers | Docker / Docker Compose |
The semantic branch additionally uses pgvector and a Python embedding service.
- Java 21.
- Docker and Docker Compose for the full
verifysuite and the optional local database. - A Neon PostgreSQL database for the normal development workflow.
- Provider configuration for the external integrations being enabled.
Use .env.example as the local template and configure the Neon JDBC URL and credentials in .env.
The JDBC URL should enable TLS with sslmode=require.
Do not commit real .env files, production secrets, private service-account keys, API keys, or tokens.
Production configuration includes the following variables. Which integration-specific values are needed depends on the features being enabled:
SPRING_PROFILES_ACTIVE
SPRING_DATASOURCE_URL
SPRING_DATASOURCE_USERNAME
SPRING_DATASOURCE_PASSWORD
PORT
JWT_SECRET
JWT_ACCESS_EXPIRATION
JWT_REFRESH_EXPIRATION
APP_MAIL_ENABLED
APP_MAIL_PROVIDER
RESEND_API_KEY
APP_MAIL_FROM
APP_MAIL_FRONTEND_BASE_URL
CLOUDINARY_CLOUD_NAME
CLOUDINARY_API_KEY
CLOUDINARY_API_SECRET
APP_CORS_ALLOWED_ORIGINS
APP_CORS_ALLOW_CREDENTIALS
APP_CORS_MAX_AGE
APP_SECURITY_TRUSTED_PROXY_HEADERS
APP_GOOGLE_AUTH_ENABLED
GOOGLE_AUTH_CLIENT_IDS
APP_UPGRADE_DOCUMENT_MAX_SIZE
APP_UPGRADE_DOCUMENT_MAX_PAGES
APP_UPGRADE_DOCUMENT_DOWNLOAD_TTL
APP_RATE_LIMIT_UPGRADE_REQUEST_CREATE_CAPACITY
APP_RATE_LIMIT_UPGRADE_REQUEST_CREATE_WINDOW
Generate a strong local JWT secret with at least 64 random characters:
openssl rand -base64 64Refer to .env.example and the application configuration when configuring additional integration settings.
Clone the repository:
git clone https://github.com/jamilhelal37/Ahadith-spring.git
cd Ahadith-springFor a fresh clone, create .env from .env.example, then replace the example settings with your development configuration.
Linux or macOS:
cp .env.example .envWindows PowerShell:
Copy-Item .env.example .envDo not overwrite an existing configured .env file.
The normal local workflow connects to the Neon datasource configured in .env; it does not start Docker PostgreSQL.
Linux or macOS:
./mvnw spring-boot:runWindows PowerShell:
.\mvnw.cmd spring-boot:run./start-local.sh also provides Neon-first startup with configuration checks and automatic local port selection. It does not replace the datasource configured in .env.
start-neon.ps1 is available on Windows and uses the same SPRING_DATASOURCE_* variables without starting Docker.
To explicitly start the local PostgreSQL service:
docker compose up -d postgresThe Compose app service is an explicit all-local stack and connects to that PostgreSQL service.
It is separate from the normal Neon workflow. Starting the database container alone does not change the datasource of an application launched separately through Maven.
Run the fast tests:
./mvnw testRun the full suite with PostgreSQL Testcontainers:
./mvnw verifyWindows PowerShell equivalents:
.\mvnw.cmd test
.\mvnw.cmd verifyDocker must be running for integration tests named *IT.java. They are not skipped automatically when Docker is unavailable.
These commands describe how to run the suite; they do not imply that every test passes in every environment.
Build the application:
./mvnw clean packageWindows PowerShell:
.\mvnw.cmd clean packageBuild the Docker image:
docker build -t ahadith:local .The canonical API base path is:
/api/v1
Legacy aliases remain temporarily only where controllers still expose them.
| Legacy alias | Canonical endpoint | Status |
|---|---|---|
/auth/** |
/api/v1/auth/** |
Deprecated alias |
/ahadith/** |
/api/v1/ahadith/** |
Deprecated alias |
/books/** |
/api/v1/books/** |
Deprecated alias |
/rawis/** |
/api/v1/rawis/** |
Deprecated alias |
/rulings/** |
/api/v1/rulings/** |
Deprecated alias |
/topics/** |
/api/v1/topics/** |
Deprecated alias |
/muhaddiths/** |
/api/v1/muhaddiths/** |
Deprecated alias |
/filterslist |
/api/v1/search/filters |
Deprecated alias |
/ahadith/search/filters |
/api/v1/search/filters |
Deprecated alias |
/me/** |
/api/v1/me/** |
Deprecated alias |
/me/search-history/** |
/api/v1/me/search-history/** |
Deprecated alias |
/scholar/** |
/api/v1/scholar/** |
Deprecated alias |
/admin/** |
/api/v1/admin/** |
Deprecated alias |
Removed search-engine aliases:
GET /search
GET /me/search
GET /api/v1/search
GET /api/v1/me/search
Email/password login:
POST /api/v1/auth/loginGoogle login:
POST /api/v1/auth/googleRequest:
{
"idToken": "GOOGLE_ID_TOKEN"
}The client sends only a Google ID token.
The backend verifies the token with Google, links or creates the local user, and returns the same project JWT response shape used by email/password login:
{
"accessToken": "...",
"refreshToken": "...",
"tokenType": "Bearer",
"expiresIn": 3600,
"user": {}
}Configure Google login with:
APP_GOOGLE_AUTH_ENABLED=false
GOOGLE_AUTH_CLIENT_IDS=
GOOGLE_AUTH_CLIENT_IDS is a comma-separated allowlist of accepted Google OAuth client IDs.
Use the Google Web Client ID as the primary audience. No client secret is required or used for direct ID token verification.
Public rawi and muhaddith list items contain only serialNumber, name, and about:
{
"serialNumber": 1,
"name": "الإمام البخاري",
"about": "نبذة عن المحدث"
}Books use nested references:
{
"id": "33333333-3333-3333-3333-333333333333",
"name": "صحيح البخاري",
"muhaddith": {
"id": "11111111-1111-1111-1111-111111111111",
"name": "الإمام البخاري"
}
}Search endpoint on main:
POST /api/v1/ahadith/searchExample request:
{
"query": "النية",
"mode": "FLEXIBLE",
"includeExplanation": true,
"bookIds": [
"33333333-3333-3333-3333-333333333333"
],
"page": 0,
"size": 20,
"sort": "RELEVANCE"
}Replace example IDs with IDs from the database being queried.
Search runs in PostgreSQL.
Authenticated users get one compatible search_history entry; anonymous users do not create history.
| Mode | Behavior on main |
|---|---|
EXACT |
Normalized phrase matching. Explanation text can be included with includeExplanation=true. |
FLEXIBLE |
Full-text search of Hadith text. includeExplanation does not expand this mode into explanations. |
For SEMANTIC and HYBRID, follow the semantic branch documentation.
GET /api/v1/me/search-history
GET /api/v1/me/search-history/search?keyword=...
DELETE /api/v1/me/search-history
DELETE /api/v1/me/search-history/{id}Create responses use canonical Location headers under /api/v1/admin/**.
DELETE /api/v1/admin/books/{id}Successful deletion returns 204. A missing book returns 404.
PUT /api/v1/admin/ahadith/{id}Relationship fields use nested reference objects:
{
"book": {
"id": "00000000-0000-0000-0000-000000000000"
},
"rawi": {
"id": "00000000-0000-0000-0000-000000000000"
},
"ruling": {
"id": "00000000-0000-0000-0000-000000000000"
},
"explaining": {
"id": "00000000-0000-0000-0000-000000000000"
},
"subValid": {
"id": "00000000-0000-0000-0000-000000000000"
}
}Use existing related-record IDs rather than the placeholder UUIDs above.
PUT /api/v1/admin/ahadith/{id} uses update DTO semantics, not full replacement.
Omitted fields and fields sent as null remain unchanged because MapStruct ignores null properties during updates.
Fields sent with valid non-null values are updated. Relationship fields are updated when the request contains a reference object with a valid id.
There is no PATCH endpoint for updating a Hadith.
These endpoints are available only to administrators:
GET /api/v1/admin/users?q=&status=&type=&page=0&size=20&sort=createdAt,desc
GET /api/v1/admin/users/{id}
PUT /api/v1/admin/users/{id}/status
PUT /api/v1/admin/users/{id}/typeUser search supports:
- Case-insensitive name/email search.
- Status and user-type filtering.
SearchResponsepagination.
The default sort is stable by createdAt DESC, then id. Page size is capped at 100.
Responses do not expose passwords, tokenVersion, or token data.
curl -X PUT "$API_BASE/api/v1/admin/users/<id>/status" \
-H "Authorization: Bearer <admin-access-token>" \
-H "Content-Type: application/json" \
-d '{"status":"disabled"}'Only active and disabled are accepted.
Status changes increment tokenVersion, revoke all refresh sessions for the target user, and invalidate old access tokens.
Administrators cannot disable their own account.
curl -X PUT "$API_BASE/api/v1/admin/users/<id>/type" \
-H "Authorization: Bearer <admin-access-token>" \
-H "Content-Type: application/json" \
-d '{"type":"scholar"}'Accepted types:
member
scholar
admin
Type changes increment tokenVersion, revoke all refresh sessions for the target user, and do not change old upgrade requests.
Administrators cannot change their own account type.
Members submit upgrade requests by uploading a PDF through the backend.
The client must not send status, filePath, Cloudinary public IDs, URLs, or asset IDs.
POST /api/v1/me/upgrade-requests
Content-Type: multipart/form-dataParts:
document=@credentials.pdf;type=application/pdf
notes=optional text
Example:
curl -X POST "$API_BASE/api/v1/me/upgrade-requests" \
-H "Authorization: Bearer <access-token>" \
-F "document=@credentials.pdf;type=application/pdf" \
-F "notes=Optional review notes"Successful creation returns 201 Created, sets the status to under_review, and stores document metadata rather than the document bytes in the database.
Example response:
{
"id": "00000000-0000-0000-0000-000000000000",
"status": "under_review",
"notes": "Optional review notes",
"reviewNotes": null,
"rejectionReason": null,
"documentAvailable": true,
"documentOriginalName": "credentials.pdf",
"documentSizeBytes": 12345,
"reviewedAt": null,
"createdAt": "2026-07-22T19:00:00",
"updatedAt": "2026-07-22T19:00:00"
}GET /api/v1/me/upgrade-requests
GET /api/v1/me/upgrade-requests/current
GET /api/v1/me/upgrade-requests/{id}/documentGET /api/v1/admin/upgrade-requests
GET /api/v1/admin/upgrade-requests/{id}
GET /api/v1/admin/upgrade-requests/{id}/document
PATCH /api/v1/admin/upgrade-requests/{id}/review
DELETE /api/v1/admin/upgrade-requests/{id}curl -X PATCH "$API_BASE/api/v1/admin/upgrade-requests/<id>/review" \
-H "Authorization: Bearer <admin-access-token>" \
-H "Content-Type: application/json" \
-d '{"decision":"APPROVE","reviewNotes":"Credentials verified"}'Temporary document links are returned only by the /document endpoints.
These responses use Cache-Control: no-store. Links expire after APP_UPGRADE_DOCUMENT_DOWNLOAD_TTL, which defaults to 5m.
Do not persist temporary links in web or mobile clients.
Normal list/detail responses do not expose Cloudinary publicId, assetId, or document URLs.
Validation:
- Accepts PDF files only.
- Checks content type, extension, and PDF magic bytes.
- Parses the document with PDFBox.
- Rejects encrypted PDFs.
- Enforces
APP_UPGRADE_DOCUMENT_MAX_SIZE. - Enforces
APP_UPGRADE_DOCUMENT_MAX_PAGES.
Cloudinary uploads use:
resource_type=raw
type=authenticated
Server-generated public IDs follow this structure:
upgrade-requests/{userId}/{randomUuid}
The documented Cloudinary setup uses:
Settings → Security → Allow delivery of PDF and ZIP files
These endpoints return SearchResponse<PublicTextDto>:
GET /api/v1/explaining?page=0&size=20
GET /api/v1/fake-ahadith?page=0&size=20Page numbering starts at 0.
size must be between 1 and 50.
Invalid pagination returns the shared ErrorResponseDto with a requestId.
- User roles are
MEMBER,SCHOLAR, andADMIN, mapped to Spring authorities. /api/v1/admin/**requires administrator access./api/v1/scholar/**requires scholar or administrator access.- Public GET catalog endpoints and
POST /api/v1/ahadith/searchare public. - Access tokens are JWTs with a
tokenVersionclaim. - Refresh tokens are rotated and stored only as hashes in
refresh_token_sessions. - Successful password reset increments
users.token_version, revokes refresh sessions, consumes reset tokens, and writes an activity log. - Google login accepts Google ID tokens only and uses Google's
subas the linked identity. - The backend does not store Google tokens or return
googleSubject. - JWT errors use the shared response fields
status,error,message,path,timestamp, andrequestId.
Keep production credentials outside source control.
Authenticated members, scholars, and administrators can update their own profile:
PUT /api/v1/meRequest:
{
"name": "User Name",
"gender": "male",
"birthDate": "2000-01-01"
}Only name, gender, and birthDate are updated.
The name is trimmed before saving.
Attempts to send email, password, type, status, avatarUrl, avatarPublicId, or tokenVersion are ignored because those fields are not part of the update DTO.
Profile updates do not revoke sessions.
PUT /api/v1/me/passwordRequest:
{
"currentPassword": "old-password",
"newPassword": "new-password"
}The current password must match.
The new password must satisfy the configured password policy and must differ from the current password.
A successful password change increments tokenVersion, revokes all refresh sessions, invalidates old access tokens, and returns:
{
"message": "Password changed successfully"
}Do not put production secrets in the repository.
Configure production values through the hosting environment.
Example configuration names and non-secret settings:
APP_CORS_ALLOWED_ORIGINS=<comma-separated production origins>
APP_CORS_ALLOW_CREDENTIALS=false
APP_CORS_MAX_AGE=1h
APP_SECURITY_TRUSTED_PROXY_HEADERS=true
APP_API_LEGACY_SUNSET=
APP_UPGRADE_DOCUMENT_MAX_SIZE=10MB
APP_UPGRADE_DOCUMENT_MAX_PAGES=20
APP_UPGRADE_DOCUMENT_DOWNLOAD_TTL=5m
APP_RATE_LIMIT_UPGRADE_REQUEST_CREATE_CAPACITY=5
APP_RATE_LIMIT_UPGRADE_REQUEST_CREATE_WINDOW=1d
X-Forwarded-For is used only when APP_SECURITY_TRUSTED_PROXY_HEADERS=true. Otherwise, the application uses remoteAddr.
Behind Render, this configuration assumes a trusted proxy cleans forwarded headers before passing requests.
When the production profile starts with proxy headers disabled, the application logs a warning and continues.
Legacy aliases include:
Deprecation: true
The Sunset header is emitted only when APP_API_LEGACY_SUNSET is configured.
Email is sent through Resend.
Keep RESEND_API_KEY secret.
Password-reset emails use:
${APP_MAIL_FRONTEND_BASE_URL}/reset-password?token=...
Verification links can use APP_MAIL_VERIFICATION_BASE_URL when a different base URL is required.
Configure Resend HTTP timeouts with:
APP_MAIL_CONNECT_TIMEOUT
APP_MAIL_READ_TIMEOUT
The database stores only token hashes.
An email verification token activates a user only when the current status is:
pending_confirmation
Users who are already active or disabled are not activated by a verification token.
Rejected verification attempts use the generic message:
Invalid or expired verification token
This avoids exposing account state through the error message.
Scheduled cleanup removes old login attempts, expired refresh sessions, and expired or consumed email/password tokens in batches.
Configuration:
APP_CLEANUP_ENABLED
APP_CLEANUP_INITIAL_DELAY
APP_CLEANUP_FIXED_DELAY
APP_CLEANUP_TOKEN_RETENTION
APP_CLEANUP_LOGIN_ATTEMPT_RETENTION
APP_CLEANUP_BATCH_SIZE
Public health endpoints:
/actuator/health
/actuator/health/**
Administrator-only endpoints:
/actuator/info
/actuator/metrics/**
/actuator/prometheus
The main branch includes:
| Migration | Purpose |
|---|---|
V1__Create_Tables.sql |
Initial schema. |
V2__indexes_triggers_functions.sql |
Indexes, triggers, and database functions. |
V3__seed_core_hadith_data.sql |
Development/demo seed content. |
V4__create_activity_log.sql |
Activity-log migration. |
V5__add_user_token_version.sql |
Account token-version support. |
V6__security_cleanup_indexes.sql |
Indexes supporting security-data cleanup. |
V7__add_upgrade_request_document_metadata.sql |
Upgrade-request document metadata. |
V8__fix_upgrade_request_relation.sql |
Upgrade-request relationship correction. |
V9__fix_missing_seed_rawi_relations.sql |
Missing narrator relationships in seeded data. |
V10__add_google_identity_to_users.sql |
Google identity linkage and password nullability for Google-created users. |
V11__deduplicate_fcm_tokens_and_add_global_unique_index.sql |
FCM token deduplication and global uniqueness. |
V12__add_login_attempts_updated_at_trigger.sql |
Login-attempt timestamp trigger. |
The semantic branch additionally includes:
V13__add_hadith_embeddings.sql
Follow that branch's setup rather than treating this migration as already present on main.
V3__seed_core_hadith_data.sql is for development and demonstration.
It is not the final production database and is not an authoritative religious reference. It is expected to be replaced or expanded through a reviewed content process.
V9__fix_missing_seed_rawi_relations.sql repairs missing narrator relationships in older seeded records without replacing relationships that were already corrected.
Do not edit migrations that have already been applied.
Introduce database changes through a new migration with a later version.
OpenAPI exposes API v1 paths only.
Public endpoints are not marked with bearer security.
The following protected route groups are marked with bearer-jwt:
/api/v1/me/**
/api/v1/scholar/**
/api/v1/admin/**
JPA entities, passwords, and searchVector are not intended as public API schemas.
Backend development: Jamil Helal.
Ahadith is a collaborative university project with separate backend, web, and mobile deliverables.
See the linked repositories for each component's source code and contribution history.