MrZaKaRiAGit

Qafiyah

Open-source repository of Arabic poetry with database dumps, REST API, and web interface.

Created: 5/6/2025Last pushed: 6/12/2026
Qafiyah

Overview

Open-source repository of Arabic poetry with database dumps, REST API, and web interface.

Tags & Topics
ArabicPoetryCultureAPIDatabaseTypeScript
View SourceVisit Website

Qafiyah

Qafiyah

Open-source repository of Arabic poetry with database dumps, REST API, and web interface.

Turborepo Docker Astro Bun TypeScript Hono Drizzle OpenAPI oRPC Valibot Scalar

Website · API · X Bot · Database Dumps

About

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.

Try it

One request, no auth, returns a random classical Arabic poem as plain text.

curl https://api.qafiyah.com/v1/poems/random?option=lines

Full schema and interactive playground: api.qafiyah.com/v1/docs.

Table of Contents

Tech Stack

Core

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

Web (apps/web)

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

API (apps/api)

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

Bot (apps/bot)

Tool Purpose
GitHub Actions Cron scheduler and runtime for the bot
twitter-api-v2 X/Twitter API client

Data Layer

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

Search

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

Tooling

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

Architecture

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
Loading

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
Loading

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.

Database

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.

Getting Started

Prerequisites

  • Bun (version pinned via the root packageManager field)
  • Docker (runs Postgres, Elasticsearch, and the apps)
  • PostgreSQL with pg_restore (only needed for restoring dumps directly; the Dockerized workflow handles this automatically)

Installation

git clone https://github.com/alwalxed/qafiyah.git
cd qafiyah
bun install

Development

bun run dev        # seeded Postgres + Elasticsearch + worker (Docker) + hot-reloading web & API

dev 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.

Scripts

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

Rate Limits and Terms of Use

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. v1 endpoints are stable. Breaking changes ship behind a new major version.
  • Attribution. Not required, but appreciated if you publish work that relies on the corpus.

Documentation

Sponsor

If Qafiyah is useful to you, support the project on GitHub Sponsors. Sponsorship funds dataset upkeep, API hosting, and ongoing maintenance.

License

Released under the MIT License.

Repository Stats

Stars
0
Forks
0
Watchers
0
Size
33.8 MB
Primary Language
TypeScript
LicenseMIT
Issues0 Open