Backend REST API for HaditsSoft, a digital hadith reader and search platform. It serves 14 major kitabs with Arabic text and Indonesian translations, provides full-text search with Indonesian stemming, and powers the frontend at haditssoft.github.io.
- Features
- Tech Stack
- Getting Started
- Environment Variables
- Project Structure
- API Overview
- Search Endpoints
- Authentication & Tokens
- Admin Panel
- Email & Verification
- Deployment
- Testing
- Known Kitab Names
- Related Documentation
- License
- Hadith data API β load hadith, chapters, books, sanad (chain), narrator biographies, scholar comments, classifications, similar hadith, and total counts.
- Smart search β search within a single kitab or across multiple kitabs concurrently; uses
LIKE, multi-keyword, and FTS5 with KBBI-based stemming for Indonesian queries. - Authentication β JWT access tokens (15 min) with rotating refresh tokens (7 days) including reuse detection.
- Email verification β 6-digit code sign-up verification with 15-minute expiry and a 2-minute resend cooldown.
- Forgot password β reset password via a 6-digit email code.
- User data β bookmarks, notes, last-read position, theme, font, and search-mode preferences.
- Admin API β admin authentication, user management, and full CRUD for hadith records per kitab.
- Utility integrations β reCAPTCHA verification with Telegram report forwarding, and an AI assistant proxy to a local opencode server.
- Operations β SQLite with WAL mode and a connection pool for concurrent reads, activity logging, and per-request query logs.
| Layer | Technology |
|---|---|
| Language | Go 1.19 |
| Web framework | Fiber v2 (Prefork enabled) |
| ORM | GORM |
| Database | SQLite via glebarez/sqlite (WAL mode) |
| Auth | golang-jwt + gofiber/jwt, bcrypt |
| Validation | go-playground/validator |
| Hermes + gomail (SMTP) | |
| Env | godotenv |
- Go 1.19 or newer
- A SQLite database file containing the hadith dataset (see Database note)
# 1. Clone the repository
git clone <your-repo-url>
cd <your-repo-dir>
# 2. Create your environment file from the example
cp .env.example .env
# 3. Edit .env and fill in at least:
# ADMIN_EMAIL, ADMIN_PASSWORD, JWT_SECRET, DB_CREDENTIAL
# 4. Run the server
go run main.goThe server listens on the port set by APP_PORT (default 8081).
# Build binary (Windows)
go build -o app
# Build for Linux (deployment)
set GOOS=linux && go build -ldflags "-s -w" -o appgo test ./...On startup the app only auto-migrates the User, Activity, BlacklistToken, and RefreshToken tables. The hadith data lives in the SQLite file referenced by DB_CREDENTIAL and must already exist with:
- One table per kitab (e.g.
ShahihBukhari,ShahihMuslimβ see Known Kitab Names). - Optional FTS5 shadow tables named
FTS<KitabName>(e.g.FTSSunanIbnuMajah) used for the Indonesian full-text search. - A
KBBItable (katakunci/artikatacolumns) used for Indonesian word stemming.
On first boot the server also creates the initial admin user from the ADMIN_EMAIL and ADMIN_PASSWORD environment variables if no user with that email exists.
All configuration is read from the environment (loaded from .env via godotenv).
| Variable | Required | Default | Description |
|---|---|---|---|
APP_NAME |
no | HaditsSoft |
Application name, used as email sender name |
APP_PORT |
yes | 8081 |
HTTP listen port |
APP_URL |
no | http://127.0.0.1:${APP_PORT} |
Public application URL |
ADMIN_EMAIL |
yes | β | Email of the initial admin user (created on first boot) |
ADMIN_PASSWORD |
yes | β | Password of the initial admin user (bcrypt-hashed) |
DB_CREDENTIAL |
yes | β | SQLite connection string (e.g. haditssoft.db?_busy_timeout=5000) |
JWT_SECRET |
yes | β | Secret used to sign JWTs; the server panics if unset |
MAIL_MAILER |
no | smtp |
Mail driver |
MAIL_HOST |
no | smtp.gmail.com |
SMTP host |
MAIL_PORT |
no | 465 |
SMTP port |
MAIL_USERNAME |
no | β | SMTP username |
MAIL_PASSWORD |
no | β | SMTP password / app password |
MAIL_ENCRYPTION |
no | ssl |
Encryption type |
MAIL_FROM_ADDRESS |
no | β | Sender address |
MAIL_FROM_NAME |
no | ${APP_NAME} |
Sender display name |
TELEGRAM_BOT_TOKEN |
no | β | Bot token for Telegram report forwarding |
TELEGRAM_CHAT_ID |
no | β | Chat/group ID to receive reports |
RECAPTCHA_KEY |
no | β | reCAPTCHA v2 secret key |
OPENCODE_URL |
no | http://127.0.0.1:4096 |
Local opencode server base URL for the AI endpoint |
OPENCODE_PROVIDER_ID |
no | opencode |
AI model provider ID |
OPENCODE_MODEL_ID |
no | deepseek-v4-flash-free |
AI model ID |
OPENCODE_AGENT |
no | plan |
opencode agent used for /ai/ask |
Note:
JWT_SECRET,ADMIN_EMAIL,ADMIN_PASSWORD, andDB_CREDENTIALare mandatory β the server fails to start without them.
βββ main.go # Entry point: wiring, middleware, route registration
βββ internal/ # Domain-driven application code
β βββ hadithdata/ # Public hadith data endpoints
β βββ search/ # Single- and multi-kitab search
β βββ auth/ # Login, logout, identity, refresh (user + admin)
β βββ user/ # Registration, verification, forgot password, profile
β βββ bookmark/ # Bookmarks
β βββ note/ # Notes
β βββ font/ # Font preference
β βββ theme/ # Theme preference
β βββ searchmode/ # Search mode preference
β βββ lastread/ # Last-read position
β βββ hadithadmin/ # Admin hadith CRUD
β βββ captcha/ # reCAPTCHA verification + Telegram reports
β βββ opencode/ # AI assistant proxy
β βββ shared/
β βββ auth/ # JWT helpers, bcrypt, token generation
β βββ database/ # Connection, migrations, scopes, entities
β βββ email/ # SMTP sender + Hermes templates
β βββ env/ # godotenv loader
β βββ middleware/ # JWT Protected/TokenOnly, IsAdmin, context
β βββ response/ # Response DTOs
β βββ utils/ # Query logging, FTS helpers, KBBI, caches
β βββ validator/ # Validation engine + custom validators
βββ models/ # GORM models + kitab index + FTS helpers
βββ validations/ # Per-domain input validation structs
βββ .env.example # Environment template
βββ AGENTS.md # Contributor / coding guidelines
Each domain under internal/ follows a handler β service β repository pattern:
internal/<domain>/
βββ handler.go # HTTP layer: parse, validate, respond
βββ service.go # Business logic
βββ repository.go # Data access (GORM)
βββ routes.go # Route registration
βββ dto.go # Request/response types
βββ handler_test.go # Handler tests
Base URL: http://<host>:<port>
Public endpoints need no authentication. Protected endpoints require:
Authorization: Bearer <access_token>
The standard error envelope is:
{
"status": "error",
"message": "<description>",
"data": null
}Validation errors return {"errors": { "<field>": "<message>" }} with HTTP 400.
| Method | Path | Description |
|---|---|---|
| GET | /loadMainData/:kitabName/:number |
Load a hadith record |
| GET | /classificationData/:kitabName/:number/:classify |
Classification data |
| GET | /loadCustomData/:kitabName/:number/:position/:actionId |
Custom data |
| GET | /loadSanadHadits/:kitabName/:number |
Sanad (chain of narration) |
| GET | /loadScholarComment/:narratorId |
Scholar comments for a narrator |
| GET | /loadTotalHadith/:kitabName/:narratorId |
Hadith totals for a narrator |
| GET | /loadSimilarHadith/:kitabName/:number |
Similar hadith |
| GET | /loadCompleteProfile/:narratorId |
Complete narrator profile |
| GET | /searchNoLain/:kitabName/:number |
Other reference numbers |
| GET | /loadBiographyData/:kitabName |
Narrator biographies |
| GET | /loadAllBooks/:kitabName |
List of books in a kitab |
| GET | /loadAllChapters/endfirst/:kitabName/:start/:vSelectedK |
Chapter list (end-first) |
| GET | /loadAllChapters/:kitabName/:start/:end |
Chapter list |
| POST | /loadListOfRawiName/ |
List of narrators |
| Method | Path | Description |
|---|---|---|
| POST | /searchHadits/:kitabName/:column |
Search a single kitab |
| POST | /searchHadits/all/:column |
Search multiple kitabs concurrently |
See Search Endpoints for request/response shapes.
| Method | Path | Auth | Description |
|---|---|---|---|
| POST | /auths/login |
β | Login, returns access + refresh tokens |
| POST | /auths/logout |
π | Blacklist current access token |
| GET | /auths/identity |
π | Current user ID + email |
| POST | /auths/refresh |
β | Rotate refresh token for a new pair |
| Method | Path | Auth | Description |
|---|---|---|---|
| POST | /users |
β | Sign up (sends 6-digit verification email) |
| POST | /users/verify |
π token | Verify email with code |
| POST | /users/verify/resend |
π token | Resend verification code |
| POST | /users/forgot-password |
β | Request password reset code |
| POST | /users/forgot-password/confirm |
β | Confirm reset with code + new password |
| GET | /users/:id |
π | Get profile |
| PUT | /users/:id |
π | Update profile |
| DELETE | /users/:id |
π | Delete account |
π = Protected() (JWT + active + blacklist checks). π token = TokenOnly() (JWT validity only, so inactive users can verify their email).
| Method | Path | Description |
|---|---|---|
| GET / PUT | /fonts |
Get / update font preference |
| GET / PUT | /theme |
Get / update theme preference |
| GET / PUT | /search-mode |
Get / update search mode preference |
| GET | /lastRead/:book_name |
Get last-read position |
| PUT | /lastRead |
Update last-read position |
| Method | Path | Description |
|---|---|---|
| POST | /bookmarks |
Create a bookmark |
| GET | /bookmarks |
List bookmark titles |
| GET | /bookmarks/:title |
Get one bookmark by title |
| GET | /bookmarks/:title/:book_name |
Get bookmark items for a book |
| PUT | /bookmarks/:title/:book_name |
Update all bookmark items |
| DELETE | /bookmarks/:title/:book_name |
Delete a bookmark group |
| Method | Path | Description |
|---|---|---|
| POST | /notes/:book_name/:hadith_id |
Create a note |
| GET | /notes/:book_name/:hadith_id |
Get a note |
| GET | /notes/validate-delete/:book_name/:hadith_id |
Check whether a note can be deleted |
| GET | /notes/:book_name |
List notes for a book |
| PUT | /notes/:book_name/:hadith_id |
Update a note |
| DELETE | /notes/:book_name/:hadith_id |
Delete a note |
| Method | Path | Auth | Description |
|---|---|---|---|
| POST | /verifyreCaptcha/ |
β | Verify a reCAPTCHA token, then forward a report to Telegram |
| POST | /ai/ask |
π | Ask the opencode-backed AI assistant |
| GET | /test |
β | Health check, returns Hello, World! |
All admin routes are prefixed with /admin.
| Method | Path | Description |
|---|---|---|
| POST | /admin/auths/login |
Admin login (Active + Admin users only) |
| POST | /admin/auths/logout |
Admin logout |
| GET | /admin/auths/identity |
Admin identity |
| POST | /admin/auths/refresh |
Rotate admin refresh token |
| GET | /admin/users |
List users (pagination + search) |
| GET | /admin/users/some?filter=... |
Get users by IDs |
| GET | /admin/users/:id |
Get a user |
| POST | /admin/users |
Create a user |
| PUT | /admin/users/:id |
Update a user |
| DELETE | /admin/users/:id |
Soft-delete a user |
| DELETE | /admin/users?ids=[...] |
Delete multiple users |
| GET | /admin/:kitabName |
List hadith records (pagination + search) |
| GET | /admin/:kitabName/:number |
Get one hadith record |
| POST | /admin/:kitabName |
Create a hadith record |
| PUT | /admin/:kitabName/:number |
Update a hadith record |
| DELETE | /admin/:kitabName/:number |
Delete a hadith record |
Two search strategies are available β the frontend picks which to call.
POST /searchHadits/:kitabName/:column
Body:
{
"keyword": ["niat", "amal"]
}Response β a JSON array:
[rows, "SEARCHRESULTCOUNT", kitabName]where rows is an array of { "<kitabIndex>": <hadithNumber> } objects.
POST /searchHadits/all/:column
Body:
{
"keyword": ["niat", "amal"],
"books": ["ShahihBukhari", "ShahihMuslim"]
}books is required (HTTP 400 if missing or empty). Searches are run concurrently via goroutines against the SQLite WAL-enabled pool.
Response:
{
"results": {
"ShahihBukhari": { "rows": [...], "count": 5 },
"ShahihMuslim": { "rows": [...], "count": 3 }
},
"total": 8
}Based on keyword count, the backend dispatches to one of:
- Single keyword β
LIKE '%keyword%'on the target column. - Multi keyword β multiple
LIKEconditions joined withAND. - Indonesian full-text (FTS5) β used when the column is
Indonesiaand more than one keyword remains after filtering; combines the kitab'sFTS<KitabName>table, KBBI stemming, phrase-variant combinations, and aLIKEfallback when FTS tables are missing or results are sparse.
Common conjunction words (atau, dan, di, yang, tentang, hadits, hadis, hadist, takhrij) are filtered out before searching. Each search writes SEARCH_START / SEARCH_RESULT entries to the daily query log.
| Property | Value |
|---|---|
| Algorithm | HS256 |
| Expiry | 15 minutes |
| Secret | JWT_SECRET env var |
| Claims | user_id, email, exp |
| Header | Authorization: Bearer <token> |
| Property | Value |
|---|---|
| Format | 64 random bytes β hex string |
| Storage | SHA-256 hash in the RefreshToken table (plain text never persisted) |
| Expiry | 7 days |
| Rotation | Each refresh marks the old token is_used = true and issues a new pair |
| Reuse detection | Presenting an already-used token revokes all of the user's refresh tokens |
1. POST /auths/login β access_token + refresh_token
2. Use access_token in headers β Authorization: Bearer <access_token>
3. When access_token expires β POST /auths/refresh with refresh_token
4. POST /auths/logout β blacklists the current access token
Protected()β validates the JWT, then checks the blacklist, the user'sactiveflag, and soft-deleted status.TokenOnly()β validates the JWT only (used so inactive users can verify their email).IsAdmin()β requiresactive = trueandadmin = true(used on all/adminroutes).
- The initial admin is created automatically on first boot from
ADMIN_EMAIL/ADMIN_PASSWORD. - Admin identity is defined by
active = trueandadmin = trueon theUsermodel. - All
/admin/*routes are guarded byProtected()+IsAdmin()exceptloginandrefresh. - Admin mutations (login, logout, user CRUD) are recorded in the
Activitytable.
- Registration sends a 6-digit verification code with a 15-minute expiry.
POST /users/verifyactivates the account (active = true).POST /users/verify/resendgenerates a new code (old one invalidated) with a 2-minute cooldown.- Forgot password uses the same code mechanism, then
POST /users/forgot-password/confirmsets the new password. - Emails are sent over SMTP (
MAIL_*env vars) using Hermes templates; if SMTP is not configured, verification features still function but no email is delivered.
# Cross-compile for Linux
set GOOS=linux && go build -ldflags "-s -w" -o appProduction hints:
- Static files β
app.Static("/", "./storage")serves uploaded content from thestorage/directory. Create it on the target machine (mkdir storage). - Database β SQLite runs in WAL mode with a 10-connection pool (set on startup), which supports the concurrent multi-kitab searches. Copy your prepared
.dbfile alongside the binary and pointDB_CREDENTIALat it. Set_busy_timeout(e.g.?_busy_timeout=5000) to avoid lock errors. - Secrets β
JWT_SECRETis mandatory; the server refuses to start without it. Never commit.env. - Prefork β Fiber runs with
Prefork: true, spawning one process per CPU core; ensure your deployment honors that (systemdKillMode=processor similar) if applicable. - Logs β a daily query log
query_YYYYMMDD.logis written to the working directory for search/query diagnostics. - Health β
GET /testreturnsHello, World!and can be used as a basic health check.
# Run the full test suite
go test ./...
# Single package
go test ./internal/auth/...
# Single test (verbose)
go test -v -run TestFunctionName ./internal/user/...The suite includes 180+ tests covering domain handlers, the migration pipeline, schema drift checks, and FTS utilities.
Valid values for the kitabName path parameter (each is its own database table):
| Value | Kitab |
|---|---|
ShahihBukhari |
Sahih al-Bukhari |
ShahihMuslim |
Sahih Muslim |
SunanTirmidzi |
Sunan al-Tirmidhi |
SunanAbuDaud |
Sunan Abu Dawud |
SunanNasai |
Sunan al-Nasa'i |
SunanIbnuMajah |
Sunan Ibn Majah |
SunanDarimi |
Sunan al-Darimi |
MusnadAhmad |
Musnad Ahmad |
MuwathaMalik |
Muwatta Malik |
SunanDaruquthni |
Sunan al-Daraqutni |
ShahihIbnuKhuzaimah |
Sahih Ibn Khuzaymah |
ShahihIbnuHibban |
Sahih Ibn Hibban |
AlMustadrak |
Al-Mustadrak ala al-Sahihayn |
MusnadSyafii |
Musnad al-Shafi'i |
- AGENTS.md β build/test commands and coding conventions for contributors.
- haditssoft.github.io β the frontend this API powers.