Skip to content

Database

The Hasanat starter uses Cloudflare D1, a SQLite database that runs on Cloudflare. Each app has its own 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.

  1. Create the database. The command prints its database_id:

    نافذة طرفيّة
    npx wrangler d1 create your-app-db
  2. Add the binding to wrangler.toml:

    [[d1_databases]]
    binding = "DB"
    database_name = "your-app-db"
    database_id = "paste-your-database-id-here"
  3. Edit schema.sql for your app.

  4. Replace YOUR_DB_NAME with your database name in the db:migrate:local and db:migrate:prod scripts in package.json, then apply the schema to the local database:

    نافذة طرفيّة
    npm run db:migrate:local

    npm run db:migrate:prod applies it to the production database. Do not run it before reviewing the change.

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-items
npx wrangler d1 migrations apply your-app-db --local
npx wrangler d1 migrations apply your-app-db --remote
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.

  • Bilingual columns for user-facing text: title_ar and title_en.
  • Dates as text, defaulting to datetime('now').
  • CHECK constraints for status fields.
  • created_by as text, holding the user ID from sign-in.
  • Indexes on columns you often filter or sort by.
  • 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: parameterized
await db.prepare("SELECT * FROM items WHERE id = ?").bind(id).all();
// Bad: SQL injection risk
await db.prepare(`SELECT * FROM items WHERE id = ${id}`).all();

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.