Database
The Hasanat starter uses Cloudflare D1, a SQLite database that runs on Cloudflare. Each app has its own database.
Your own database or a Hasanat database
Section titled “Your own database or a Hasanat database”- In your Cloudflare account: create the database yourself with the steps below. This fits when you deploy the app from your own account.
- In the Hasanat account: if your app will run in Hasanat’s Cloudflare account, request a database from the platform dashboard. A maintainer creates it and sends you its name and ID in the request’s note. The name and ID are not secrets; never send a secret in a request. Agree in the same request on who applies schema changes to the database.
The shared hasanat-db database belongs to hasanat.dev and this platform, and its schema lives in the hasanat repository. Do not use it for your project’s app.
Setup in the starter
Section titled “Setup in the starter”-
Create the database. The command prints its
database_id:نافذة طرفيّة npx wrangler d1 create your-app-db -
Add the binding to
wrangler.toml:[[d1_databases]]binding = "DB"database_name = "your-app-db"database_id = "paste-your-database-id-here" -
Edit
schema.sqlfor your app. -
Replace
YOUR_DB_NAMEwith your database name in thedb:migrate:localanddb:migrate:prodscripts inpackage.json, then apply the schema to the local database:نافذة طرفيّة npm run db:migrate:localnpm run db:migrate:prodapplies it to the production database. Do not run it before reviewing the change.
Schema changes after launch
Section titled “Schema changes after launch”A single schema.sql is enough at the start. Once the database holds real data, keep each change in its own migration file so the history stays clear:
npx wrangler d1 migrations create your-app-db add-itemsnpx wrangler d1 migrations apply your-app-db --localnpx wrangler d1 migrations apply your-app-db --remoteUsing it in code
Section titled “Using it in code”import { getDB, queryAll, queryOne, execute } from "@/lib/db";
const db = getDB(Astro.locals);
const items = await queryAll(db, "SELECT * FROM items WHERE status = ?", "active");const item = await queryOne(db, "SELECT * FROM items WHERE id = ?", id);await execute(db, "INSERT INTO items (title_ar, created_by) VALUES (?, ?)", title, user.id);The starter uses Astro 5, which reads bindings from Astro.locals.runtime.env. Astro 6 and later removed Astro.locals.runtime: import env from cloudflare:workers and use env.DB.
Schema conventions
Section titled “Schema conventions”- Bilingual columns for user-facing text:
title_arandtitle_en. - Dates as text, defaulting to
datetime('now'). CHECKconstraints for status fields.created_byas text, holding the user ID from sign-in.- Indexes on columns you often filter or sort by.
Security
Section titled “Security”- Always use parameterized queries; never put user input into SQL text.
- Validate input with zod before writing.
- Check that the user is signed in before any write, and return only the data they may see.
- Never put real names or numbers in tests or sample data.
// Good: parameterizedawait db.prepare("SELECT * FROM items WHERE id = ?").bind(id).all();
// Bad: SQL injection riskawait db.prepare(`SELECT * FROM items WHERE id = ${id}`).all();Limits
Section titled “Limits”According to Cloudflare’s documentation, the free plan allows 5 million rows read and 100,000 rows written per day, 5 GB of storage per account, 500 MB per database, and 10 databases per account. Check the official page for current values.