Open-source repository of Arabic poetry with database dumps, REST API, and web interface.
Website · API · X Bot · Database Dumps
Qafiyah is an open-source corpus of classical Arabic poetry spanning the major historical eras. It offers full-text Arabic search (Elasticsearch); faceted browsing by era, meter, rhyme letter, theme, and collection; a public REST API with auto-generated OpenAPI docs; and downloadable PostgreSQL dumps. An X bot posts a random poems daily. Built for readers, researchers, and developers working with classical Arabic literature.
One request, no auth, returns a random classical Arabic poem as plain text.
curl https://api.qafiyah.com/v1/poems/random?option=linesFull schema and interactive playground: api.qafiyah.com/v1/docs.
| Tool | Purpose |
|---|---|
| Bun | Package manager and JavaScript runtime |
| Turborepo | Monorepo task orchestration and build caching |
| TypeScript | Language across all packages |
| envin | Type-safe environment variable loading and parsing |
| ts-pattern | Exhaustive, type-safe pattern matching used across all apps |
| neverthrow | Typed Result for fallible logic at module boundaries |
| Tool | Purpose |
|---|---|
| Astro | SSR framework (output: 'server'); pages rendered per request |
| @astrojs/node | Standalone Node adapter; runs the SSR server under Bun |
| React | Interactive islands (search, nav, random poem) |
| TailwindCSS | Utility-first CSS |
| Radix Slot | Polymorphic-render primitive for component composition |
| TanStack Query | Server-state and data-fetching in React islands |
| nuqs | Type-safe URL search-param state for React islands |
| lucide-react | Icon set used throughout the UI |
| clsx + tailwind-merge | Conditional class composition with Tailwind conflict resolution |
| class-variance-authority | Typed variant API for component styling |
| Tool | Purpose |
|---|---|
| Hono | Lightweight HTTP framework running on a Bun server (Docker) |
| oRPC | Type-safe RPC with shared contracts |
| Valibot | Schema validation for all oRPC contract inputs and outputs |
| OpenAPI | API spec auto-generated from oRPC contracts via @orpc/openapi |
| Scalar | Interactive API documentation served at /v1/docs |
| Docker | Container runtime; API and web served via Docker Compose |
| Tool | Purpose |
|---|---|
| GitHub Actions | Cron scheduler and runtime for the bot |
| twitter-api-v2 | X/Twitter API client |
| Tool | Purpose |
|---|---|
| Drizzle ORM | SQL query builder and schema definitions in packages/db |
| postgres.js | Underlying Postgres client that Drizzle wraps |
| PostgreSQL | Primary datastore and source of truth (browsing queries, dump source) |
| Docker | Local Postgres and Elasticsearch containers for development |
| Tool | Purpose |
|---|---|
| Elasticsearch | Full-text Arabic search engine; powers /v1/search (packages/search) |
| @elastic/elasticsearch | ES client; forced HttpConnection transport for Bun compatibility |
| apps/worker | Syncs Postgres → Elasticsearch: boot reindex (if empty) + weekly reconcile |
| Tool | Purpose |
|---|---|
| Biome | Linting and formatting for all JS/TS files |
| Prettier | Formatting for non-JS assets |
| Vitest | Unit and integration tests across all workspaces |
| Husky | Git hooks |
| commitlint | Conventional commit enforcement |
| Knip | Detection of unused files, dependencies, and exports |
| Madge | Circular dependency detection |
| dependency-cruiser | Architectural import rules across apps/ and packages/ |
| Syncpack | Cross-workspace dependency version consistency |
Bun + Turborepo monorepo: four apps and five shared packages.
qafiyah/
├── apps/
│ ├── web/ Astro on-demand SSR (Bun + @astrojs/node) behind nginx proxy_cache; renders each route per request by fetching the API via oRPC
│ ├── api/ Hono REST API (browsing + /v1/search), Bun server (Docker container)
│ ├── worker/ Postgres→Elasticsearch sync (boot reindex + weekly reconcile), Bun server (Docker container)
│ └── bot/ X/Twitter bot; posts 4× daily via GitHub Actions cron
└── packages/
├── db/ Drizzle ORM schema, queries, and Postgres client factory
├── search/ Elasticsearch client, Arabic analyzers/mappings, query + reindex/reconcile
├── contracts/ Shared oRPC contract definitions
├── constants/ Shared brand, URLs, and dev-port constants
└── typescript/ Shared TypeScript configs (base, astro, bun)
Package dependencies, who imports whom at compile time:
graph TD
subgraph APPS
WEB["apps/web\nAstro · React islands"]
API["apps/api\nHono · Bun server (Docker)"]
WORKER["apps/worker\nPG→ES sync · Bun (Docker)"]
BOT["apps/bot\nGitHub Actions cron"]
end
subgraph PACKAGES
DB["packages/db\nDrizzle ORM · queries"]
SEARCH["packages/search\nElasticsearch · Arabic analyzers"]
CONTRACTS["packages/contracts\noRPC · Valibot schemas"]
CONSTANTS["packages/constants"]
end
WEB --> CONTRACTS & CONSTANTS
API --> DB & SEARCH & CONTRACTS & CONSTANTS
WORKER --> DB & SEARCH & CONSTANTS
BOT --> CONTRACTS & CONSTANTS
DB --> SEARCH & CONTRACTS & CONSTANTS
SEARCH --> CONSTANTS
CONTRACTS --> CONSTANTS
packages/typescript ships shared tsconfig presets (base, astro, bun) consumed via extends, not code imports; omitted from the graph above.
Two architectural constraints. packages/db is consumed only by apps/api and apps/worker; no Drizzle or Postgres imports exist under apps/web or apps/bot. apps/web has no DB access: it renders each route on demand (SSR), querying the API per request through server-only oRPC accessors in src/lib/server/ (pointed at INTERNAL_API_URL), while browser islands fetch the public API via src/lib/api/ (rpc.ts, client.ts, orpc.ts).
Runtime data flow, how requests move once deployed:
graph LR
BROWSER["Browser"]
BOT["apps/bot"]
GHA(["GitHub Actions\ncron 4×/day"])
TW(["X / Twitter"])
subgraph VPS["Docker on VPS · docker compose"]
subgraph WEBIMG["web image"]
NGINX["nginx\nproxy_cache · static assets"]
WEB["Astro SSR · Bun\napps/web"]
end
API["apps/api\nHono · Bun"]
WORKER["apps/worker\nPG→ES sync"]
PG[("PostgreSQL")]
ES[("Elasticsearch")]
end
BROWSER -->|"page loads"| NGINX
NGINX -->|"proxy 127.0.0.1:4321"| WEB
BROWSER -->|"island data · search, random"| API
WEB -->|"per-request SSR oRPC · INTERNAL_API_URL"| API
API -->|"browsing · @qafiyah/db · SQL"| PG
API -->|"/v1/search · @qafiyah/search"| ES
WORKER -->|"read · @qafiyah/db"| PG
WORKER -->|"reindex / reconcile"| ES
GHA --> BOT
BOT -->|"GET /poems/random"| API
BOT -->|"post tweet"| TW
Everything inside Docker on VPS ships from docker-compose.yml (docker compose up -d --build or bun run deploy): Postgres, Elasticsearch, api, worker, and the web image. The web image bundles nginx (proxy_cache + static assets) in front of the Astro SSR server; the SSR server reaches api over the internal network (INTERNAL_API_URL). The api reads Postgres for browsing and Elasticsearch for /v1/search; the worker keeps Elasticsearch in sync with Postgres (boot reindex + weekly reconcile). Browser islands call the public API directly. The bot runs on GitHub Actions, outside the VPS.
The corpus is modeled as poems and verses, organized by poet, era, meter, rhyme letter, theme, and collection. Live counts come from the data, not this README.
PostgreSQL custom-format dumps are published in dumps/ and refreshed periodically. They are provided for research and integration as an alternative to scraping the API; see the restore instructions.
- Bun (version pinned via the root
packageManagerfield) - Docker (runs Postgres, Elasticsearch, and the apps)
- PostgreSQL with
pg_restore(only needed for restoring dumps directly; the Dockerized workflow handles this automatically)
git clone https://github.com/alwalxed/qafiyah.git
cd qafiyah
bun installbun run dev # seeded Postgres + Elasticsearch + worker (Docker) + hot-reloading web & APIdev brings up the database and Elasticsearch containers (DB auto-seeded on first boot; the worker reindexes Elasticsearch if its alias is empty), writes the local .env files, then starts the dev servers. For a full containerized run, use bun run up.
Dev and build
| Script | Description |
|---|---|
bun run dev |
Seeded Postgres + Elasticsearch + worker (Docker) + web & API hot reload |
bun run up |
Build + run the full stack in Docker (DB self-seeds on first boot) |
bun run down |
Stop the Docker stack |
bun run deploy |
Push-button production deploy: sync the VPS to origin/main and rebuild |
bun run build |
Build all workspaces |
bun run db:up |
Start just the Postgres container (auto-seeds on a fresh volume) |
bun run db:reset |
Wipe the DB volume and re-seed from the latest dump |
bun run es:up |
Start just the Elasticsearch container |
bun run es:down |
Stop the Elasticsearch container |
bun run reindex |
Rebuild the Elasticsearch index from Postgres and swap the alias |
bun run clean |
Kill orphan Astro and API server processes from prior dev runs |
Quality
| Script | Description |
|---|---|
bun run test |
Run Vitest across all workspaces |
bun run types |
Type-check all workspaces with tsc --noEmit |
bun run lint |
Lint and auto-fix with Biome |
bun run format |
Format JS/TS with Biome and Markdown/MDX with Prettier |
bun run knip |
Detect unused files, dependencies, and exports |
bun run madge |
Detect circular imports across apps/ and packages/ |
bun run depcruise |
Run dependency-cruiser against the architectural rules in .dependency-cruiser.cjs |
Boundary checks
| Script | Description |
|---|---|
bun run check:boundaries |
Forbid cross-app imports (apps may not import from each other) |
bun run check:naming |
Enforce project-wide naming conventions on files and identifiers |
bun run check:no-parent-imports |
Forbid ../ imports anywhere; siblings or @/ aliases only |
bun run check:api-db-isolation |
Forbid Drizzle or postgres imports outside packages/db |
bun run check:constants |
Ensure brand strings, URLs, and ports live in packages/constants, not in app code |
bun run check:syncpack |
Verify dependency versions are consistent across all workspaces |
Aggregate and utilities
| Script | Description |
|---|---|
bun run ci |
Full pipeline: format and lint sequentially, then run types, test, knip, madge, all six boundary checks, depcruise, bun audit, and the API smoke test in parallel |
bun run smoke |
Spin up the API locally and hit each public endpoint to catch breakage in the request path |
bun run deps:doctor |
Diagnose and update workspace dependencies |
bun run optimize:images |
Convert raster images in the repo to sibling .webp files using Bun.Image |
bun --filter @qafiyah/web run verify:seo |
Crawl the running web server and assert SEO parity (canonical URLs, JSON-LD, metadata) across rendered pages |
The API is free, requires no authentication, and is provided on a best-effort basis with no SLA.
- Fair use. Per-IP throttling is enforced at the server level. For bulk access, prefer the PostgreSQL dumps over paginating the API.
- Caching. Responses are cacheable; cache them client-side when possible to reduce load.
- Stability.
v1endpoints are stable. Breaking changes ship behind a new major version. - Attribution. Not required, but appreciated if you publish work that relies on the corpus.
If Qafiyah is useful to you, support the project on GitHub Sponsors. Sponsorship funds dataset upkeep, API hosting, and ongoing maintenance.
Released under the MIT License.