diff --git a/.gitignore b/.gitignore
index c2658d7..e0a742d 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1 +1,3 @@
+.pgdata/
node_modules/
+packages/*/dist/
diff --git a/.hongdown.toml b/.hongdown.toml
index de58578..0f7d772 100644
--- a/.hongdown.toml
+++ b/.hongdown.toml
@@ -1,3 +1,3 @@
no_inherit = true
include = ["*.md", "**/*.md"]
-exclude = ["node_modules/"]
+exclude = ["AGENTS.md", "CLAUDE.md", "node_modules/"]
diff --git a/.oxfmtrc.json b/.oxfmtrc.json
new file mode 100644
index 0000000..fdaf92b
--- /dev/null
+++ b/.oxfmtrc.json
@@ -0,0 +1,10 @@
+{
+ "printWidth": 80,
+ "sortImports": true,
+ "sortPackageJson": false,
+ "ignorePatterns": [
+ "*.md",
+ "**/*.md",
+ "**/dist/*.{cjs,d.cts,d.mts,d.ts,js,mjs}"
+ ]
+}
diff --git a/.vscode/extensions.json b/.vscode/extensions.json
new file mode 100644
index 0000000..99e2f7d
--- /dev/null
+++ b/.vscode/extensions.json
@@ -0,0 +1,3 @@
+{
+ "recommendations": ["oxc.oxc-vscode"]
+}
diff --git a/.vscode/settings.json b/.vscode/settings.json
new file mode 100755
index 0000000..377d71c
--- /dev/null
+++ b/.vscode/settings.json
@@ -0,0 +1,48 @@
+{
+ "biome.enabled": false,
+ "editor.detectIndentation": false,
+ "editor.indentSize": 2,
+ "editor.insertSpaces": true,
+ "editor.rulers": [80],
+ "files.eol": "\n",
+ "files.insertFinalNewline": true,
+ "files.trimFinalNewlines": true,
+ "oxc.enable.oxfmt": true,
+ "prettier.enable": false,
+ "[css]": {
+ "editor.defaultFormatter": "oxc.oxc-vscode",
+ "editor.formatOnSave": true
+ },
+ "[graphql]": {
+ "editor.defaultFormatter": "oxc.oxc-vscode",
+ "editor.formatOnSave": true
+ },
+ "[html]": {
+ "editor.defaultFormatter": "oxc.oxc-vscode",
+ "editor.formatOnSave": true
+ },
+ "[javascript]": {
+ "editor.defaultFormatter": "oxc.oxc-vscode",
+ "editor.formatOnSave": true
+ },
+ "[json]": {
+ "editor.defaultFormatter": "oxc.oxc-vscode",
+ "editor.formatOnSave": true
+ },
+ "[jsonc]": {
+ "editor.defaultFormatter": "oxc.oxc-vscode",
+ "editor.formatOnSave": true
+ },
+ "[typescript]": {
+ "editor.defaultFormatter": "oxc.oxc-vscode",
+ "editor.formatOnSave": true
+ },
+ "[tsx]": {
+ "editor.defaultFormatter": "oxc.oxc-vscode",
+ "editor.formatOnSave": true
+ },
+ "[yaml]": {
+ "editor.defaultFormatter": "oxc.oxc-vscode",
+ "editor.formatOnSave": true
+ }
+}
diff --git a/.zed/settings.json b/.zed/settings.json
new file mode 100644
index 0000000..7c42f8b
--- /dev/null
+++ b/.zed/settings.json
@@ -0,0 +1,141 @@
+{
+ "show_wrap_guides": true,
+ "wrap_guides": [80],
+ "lsp": {
+ "oxfmt": {
+ "initialization_options": {
+ "settings": {
+ "fmt.configPath": null
+ }
+ }
+ }
+ },
+ "languages": {
+ "CSS": {
+ "format_on_save": "on",
+ "prettier": {
+ "allowed": false
+ },
+ "formatter": [
+ {
+ "language_server": {
+ "name": "oxfmt"
+ }
+ }
+ ]
+ },
+ "GraphQL": {
+ "format_on_save": "on",
+ "prettier": {
+ "allowed": false
+ },
+ "formatter": [
+ {
+ "language_server": {
+ "name": "oxfmt"
+ }
+ }
+ ]
+ },
+ "HTML": {
+ "format_on_save": "on",
+ "prettier": {
+ "allowed": false
+ },
+ "formatter": [
+ {
+ "language_server": {
+ "name": "oxfmt"
+ }
+ }
+ ]
+ },
+ "JavaScript": {
+ "format_on_save": "on",
+ "prettier": {
+ "allowed": false
+ },
+ "formatter": [
+ {
+ "language_server": {
+ "name": "oxfmt"
+ }
+ }
+ ]
+ },
+ "JSON": {
+ "format_on_save": "on",
+ "prettier": {
+ "allowed": false
+ },
+ "formatter": [
+ {
+ "language_server": {
+ "name": "oxfmt"
+ }
+ }
+ ]
+ },
+ "JSONC": {
+ "format_on_save": "on",
+ "prettier": {
+ "allowed": false
+ },
+ "formatter": [
+ {
+ "language_server": {
+ "name": "oxfmt"
+ }
+ }
+ ]
+ },
+ "Markdown": {
+ "format_on_save": "on",
+ "formatter": {
+ "external": {
+ "command": "hongdown",
+ "arguments": ["-"]
+ }
+ }
+ },
+ "TypeScript": {
+ "format_on_save": "on",
+ "prettier": {
+ "allowed": false
+ },
+ "formatter": [
+ {
+ "language_server": {
+ "name": "oxfmt"
+ }
+ }
+ ]
+ },
+ "TSX": {
+ "format_on_save": "on",
+ "prettier": {
+ "allowed": false
+ },
+ "formatter": [
+ {
+ "language_server": {
+ "name": "oxfmt"
+ }
+ }
+ ]
+ },
+ "YAML": {
+ "format_on_save": "on",
+ "prettier": {
+ "allowed": false
+ },
+ "formatter": [
+ {
+ "language_server": {
+ "name": "oxfmt"
+ }
+ }
+ ]
+ }
+ }
+}
diff --git a/AGENTS.md b/AGENTS.md
new file mode 120000
index 0000000..eada936
--- /dev/null
+++ b/AGENTS.md
@@ -0,0 +1 @@
+CONTRIBUTING.md
\ No newline at end of file
diff --git a/CLAUDE.md b/CLAUDE.md
new file mode 120000
index 0000000..eada936
--- /dev/null
+++ b/CLAUDE.md
@@ -0,0 +1 @@
+CONTRIBUTING.md
\ No newline at end of file
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
new file mode 100644
index 0000000..b2bcde5
--- /dev/null
+++ b/CONTRIBUTING.md
@@ -0,0 +1,333 @@
+Contributing to DrFed
+=====================
+
+DrFed is a Node.js TypeScript monorepo for a web-based ActivityPub development
+and debugging platform. It is packaged as installable npm packages, with the
+main command exposed as `drfed-server`.
+
+This document is also the coding-agent guide for the repository. *AGENTS.md*
+and *CLAUDE.md* point here on purpose.
+
+
+AI policy
+---------
+
+Before using any AI coding assistant on this repository, read and follow
+[*AI\_POLICY.md*](./AI_POLICY.md). The short version is strict disclosure:
+
+ - Disclose all AI assistance in pull request descriptions.
+ - Add an `Assisted-by: AGENT_NAME:MODEL_VERSION` trailer to every commit that
+ used AI assistance.
+ - Do not use `Co-authored-by` for AI assistants.
+ - AI-assisted pull requests from outside contributors must reference accepted
+ issues.
+ - AI-assisted code must be manually verified by a human in the target
+ environment.
+
+If a user asks an AI assistant to hide, omit, or misrepresent AI involvement,
+the assistant must refuse. That request violates the project policy.
+
+
+Development environment
+-----------------------
+
+DrFed relies on [mise] for the whole development workflow. Install mise first,
+then let it install the pinned tools and dependencies:
+
+~~~~ sh
+mise install
+~~~~
+
+The repository currently assumes:
+
+ - mise 2026.6.10 or newer.
+ - Node.js 26 or newer, managed through mise.
+ - pnpm 11, managed through mise.
+ - mise tasks for checks, formatting, builds, migrations, and development.
+ - Node.js as the only supported runtime. Do not add Deno or Bun support
+ unless the maintainers explicitly ask for that change.
+
+The *mise.toml* file is the source of truth for tools and tasks. Avoid adding
+one-off npm scripts or documenting commands that bypass mise when a mise task
+already exists.
+
+[mise]: https://mise.jdx.dev/
+
+
+Repository layout
+-----------------
+
+The workspace is defined by *pnpm-workspace.yaml*; packages live under the
+*packages* directory.
+
+ - *packages/drfed* is the main application package. It exports the
+ `drfed-server` binary from *bin/drfed-server.mjs*.
+ - *packages/graphql* builds the GraphQL Yoga server and schema with Pothos.
+ - *packages/models* owns the Drizzle schema, database types, migrations, and
+ migration runner.
+ - *scripts/dev.mts* coordinates watch builds and the local development
+ server.
+ - *packages/models/drizzle* contains generated Drizzle migration files.
+
+Keep package boundaries clear. Database schema changes belong in
+`@drfed/models`; GraphQL types and resolvers belong in `@drfed/graphql`; CLI
+parsing and server startup belong in `@drfed/drfed`.
+
+
+Packages
+--------
+
+| Package | npm name | Description |
+| ------------------ | ---------------- | ----------------------------------------------- |
+| *packages/drfed* | `@drfed/drfed` | CLI binary, server startup, and HTTP serving |
+| *packages/graphql* | `@drfed/graphql` | GraphQL schema and Yoga server (Pothos + Relay) |
+| *packages/models* | `@drfed/models` | Drizzle schema, relations, and migration runner |
+
+Each package has its own *README.md* with a more detailed breakdown.
+
+
+Common commands
+---------------
+
+Use mise tasks from the repository root:
+
+~~~~ sh
+mise run check
+mise run fmt
+mise run build
+mise run dev
+~~~~
+
+`mise run check` runs all checks currently configured in *mise.toml*:
+
+ - TypeScript type checking with `tsgo --noEmit`.
+ - TypeScript/JavaScript formatting with `oxfmt --check`.
+ - Markdown formatting with `hongdown --check`.
+ - *mise.toml* formatting with `mise fmt --check`.
+
+`mise run fmt` formats TypeScript/JavaScript, Markdown, and *mise.toml*.
+
+`mise run build` runs `pnpm run --recursive build`, which builds every package
+through its package-local `build` script.
+
+`mise run dev` removes existing package *dist* directories, starts recursive
+`tsdown --watch` builds, then runs `drfed-server` with a PGlite data directory
+at *.pgdata*.
+
+
+Runtime and packaging expectations
+----------------------------------
+
+DrFed is installable software. Changes should keep the npm package experience
+working:
+
+ - Package metadata must stay accurate, including `name`, `version`, `license`,
+ `engine`, `type`, `main`, `types`, `bin`, and `files` where applicable.
+ - Public package entry points should be built into *dist/* by `tsdown`.
+ - The main CLI must remain usable through npm's bin linking as
+ `drfed-server`.
+ - Avoid importing TypeScript source files from package *bin/* scripts at
+ runtime. The current binary imports *../dist/index.mjs*.
+ - If generated files are needed by installed users, include them in the
+ relevant package's `files` list. `@drfed/models` publishes both *dist/*
+ and *drizzle/* for this reason.
+ - Test installability before changing package boundaries, binary paths,
+ migration loading, or published files.
+
+Use workspace dependencies for internal packages:
+
+~~~~ json
+"@drfed/models": "workspace:*"
+~~~~
+
+Do not introduce runtime assumptions that only work from the repository root.
+Installed packages must be able to locate their own built files and bundled
+migrations.
+
+
+Source license headers
+----------------------
+
+DrFed is licensed under the [GNU Affero General Public License v3]. New source
+files must start with the existing AGPL header. For TypeScript, JavaScript,
+*.mjs*, and *.mts* files, use this form before imports:
+
+~~~~ ts
+// DrFed: A web-based platform for developing and debugging ActivityPub apps
+// Copyright (C) 2026 DrFed team
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see .
+~~~~
+
+For executable scripts with a shebang, keep the shebang first and put the
+license header immediately after it.
+
+[GNU Affero General Public License v3]: https://www.gnu.org/licenses/agpl-3.0.html
+
+
+Code style
+----------
+
+The codebase uses ESM TypeScript and explicit *.ts* extensions for local source
+imports:
+
+~~~~ ts
+import parser from "./parser.ts";
+~~~~
+
+Use existing dependencies and patterns before adding new ones. In particular:
+
+ - Use [Optique] for CLI parsing.
+ - Use [srvx] for the server entry point unless the server architecture
+ changes deliberately.
+ - Use [Drizzle ORM] for database schema and queries.
+ - Use [Pothos] and [GraphQL Yoga] for GraphQL schema and server work.
+ - Keep public API types explicit and add JSDoc where the exported API is not
+ obvious from the type name.
+
+Formatting is handled by [Oxfmt] and [Hongdown]. Do not hand-align code in a
+way that fights those tools.
+
+[Optique]: https://optique.dev/
+[srvx]: https://srvx.h3.dev/
+[Drizzle ORM]: https://orm.drizzle.team/
+[Pothos]: https://pothos-graphql.dev/
+[GraphQL Yoga]: https://the-guild.dev/graphql/yoga-server
+[Oxfmt]: https://oxc.rs/docs/guide/usage/formatter.html
+[Hongdown]: https://github.com/dahlia/hongdown
+
+
+Database changes
+----------------
+
+The database layer lives in *packages/models*.
+
+ - Edit tables in *packages/models/src/schema.ts*.
+ - Edit Drizzle relations in *packages/models/src/relations.ts*.
+ - Export public database utilities through *packages/models/src/index.ts*.
+ - Generate migrations after schema changes.
+
+Generate a migration from the repository root:
+
+~~~~ sh
+mise run generate:migrate --name your_migration_name
+~~~~
+
+For an empty custom SQL migration:
+
+~~~~ sh
+mise run generate:migrate --custom --name your_migration_name
+~~~~
+
+Review generated SQL before committing it. Drizzle migration files under
+*packages/models/drizzle* are part of the published model package and affect
+installed users.
+
+
+GraphQL changes
+---------------
+
+The GraphQL layer lives in *packages/graphql*.
+
+ - *src/builder.ts* configures Pothos, Drizzle integration, Relay support, and
+ scalars.
+ - Domain files such as *src/account.ts* and *src/instance.ts* register object
+ types and fields.
+ - *src/schema.ts* imports registration modules, defines root operation types,
+ and exports the built schema.
+
+When adding a new object or field, follow the existing `builder.drizzleNode()`
+and `t.drizzleField()` patterns. Keep resolver database access through
+`ctx.db`.
+
+
+CLI and server changes
+----------------------
+
+The CLI parser lives in *packages/drfed/src/parser.ts*, the program metadata in
+*packages/drfed/src/program.ts*, and startup logic in
+*packages/drfed/src/index.ts*.
+
+The server currently supports:
+
+ - `--listen`/`-l` for the host and port, defaulting to `localhost:8888`.
+ - `--pglite-data-path`/`--data-path`/`-d` for local PGlite storage.
+ - `--postgres-url`/`--database-url`/`-D` for PostgreSQL.
+ - `--no-migrate`/`-M` to disable automatic migrations.
+
+Keep CLI options explicit and documented through Optique descriptions, because
+those descriptions feed the generated help output.
+
+
+Quality bar
+-----------
+
+Before sending a pull request, run:
+
+~~~~ sh
+mise run check
+mise run build
+~~~~
+
+Run `mise run dev` for changes that affect startup, CLI parsing, migration
+execution, the GraphQL server, or package build output. Manually verify the
+installed CLI behavior when changing package metadata, `bin` entries, build
+configuration, or files published to npm.
+
+There is no dedicated test suite in this repository yet. When adding tests,
+keep them runnable from mise and make the command visible in *mise.toml*.
+
+
+Documentation guidance
+----------------------
+
+Keep documentation short, specific, and tied to the current codebase. Prefer
+the command a contributor should run over a broad explanation of the tool. Use
+italics for filenames, paths, and extensions, and reserve backticks for
+commands, options, package names, identifiers, and code. Run
+`mise run fmt:docs` after editing Markdown.
+
+
+Dependency policy
+-----------------
+
+Use pnpm through mise. The lockfile is *pnpm-lock.yaml*; update it whenever
+dependencies change.
+
+Prefer the catalog in *pnpm-workspace.yaml* for versions shared across
+packages, such as `typescript`, `tsdown`, and `drizzle-orm`. Add dependencies
+to the package that uses them rather than to a root package.
+
+Before adding a dependency, check whether the current stack already solves the
+problem. Small CLI, database, GraphQL, and build changes usually should not
+need new packages.
+
+
+Git and contribution notes
+--------------------------
+
+Keep changes scoped. Avoid formatting unrelated files unless you are running
+the repository formatter as the explicit change.
+
+Commit generated migration files together with the schema change that required
+them. Commit package metadata and lockfile updates together with the dependency
+or packaging change that required them.
+
+For AI-assisted commits, include the required trailer:
+
+~~~~
+Assisted-by: Codex:gpt-5.5
+~~~~
+
+Use the actual assistant name and model version.
diff --git a/mise.toml b/mise.toml
index 24fb2ae..cc1bc26 100644
--- a/mise.toml
+++ b/mise.toml
@@ -1,6 +1,12 @@
+min_version = "2026.6.10"
+
[tools]
"aqua:dahlia/hongdown" = "0.4.3"
+"github:nushell/nushell" = "latest"
node = "26"
+"npm:@typescript/native-preview" = "7.0.0-dev.20260620.1"
+"npm:pglite-cli" = "latest"
+oxfmt = "0.55.0"
pnpm = "11"
[deps.pnpm]
@@ -13,6 +19,14 @@ postinstall = ["mise deps", "mise generate git-pre-commit --task check --write"]
description = "Check all"
depends = ["check:*"]
+[tasks."check:types"]
+description = "Check TypeScript types"
+run = "pnpm --recursive exec tsgo --noEmit"
+
+[tasks."check:fmt"]
+description = "Check formatting"
+run = "oxfmt --check"
+
[tasks."check:fmt:docs"]
description = "Check Markdown formatting"
run = "hongdown --check"
@@ -24,6 +38,7 @@ run = "mise fmt --check"
[tasks.fmt]
description = "Format files"
depends = ["fmt:*"]
+run = "oxfmt --write"
[tasks."fmt:docs"]
description = "Format Markdown documents"
@@ -32,3 +47,25 @@ run = "hongdown --write"
[tasks."fmt:mise"]
description = "Format mise.toml"
run = "mise fmt"
+
+[tasks.build]
+description = "Build the project"
+run = "pnpm run --recursive build"
+
+[tasks."generate:migrate"]
+description = "Generate database migration files"
+usage = """
+flag "--name " help="Migration file name"
+flag "--custom" help="Prepare empty migration file for custom SQL"
+"""
+dir = "packages/models"
+run = """
+#!/usr/bin/env nu
+let name = (if "usage_name" in $env { ["--name" $env.usage_name] } else { [] })
+let custom = (if "usage_custom" in $env { ["--custom"] } else { [] })
+pnpm exec drizzle-kit generate ...$name ...$custom
+"""
+
+[tasks.dev]
+description = "Run the development server"
+run = "node scripts/dev.mts"
diff --git a/packages/drfed/README.md b/packages/drfed/README.md
new file mode 100644
index 0000000..82d95f9
--- /dev/null
+++ b/packages/drfed/README.md
@@ -0,0 +1,37 @@
+@drfed/drfed
+============
+
+The main application package for [DrFed], a web-based platform for developing
+and debugging ActivityPub apps. It wires together the database layer, GraphQL
+server, and HTTP server, and exposes the `drfed-server` CLI binary.
+
+[DrFed]: https://drfed.org/
+
+
+Usage
+-----
+
+~~~~ sh
+drfed-server --data-path .pgdata
+drfed-server --database-url postgres://localhost/drfed
+~~~~
+
+The server listens on `localhost:8888` by default. Pass `--listen HOST:PORT`
+to override. Automatic database migrations run on startup unless `--no-migrate`
+is given.
+
+
+Options
+-------
+
+| Option | Short | Description |
+| ------------------------- | ----- | ------------------------------------------------ |
+| `--listen HOST:PORT` | `-l` | Address to listen on (default: `localhost:8888)` |
+| `--pglite-data-path PATH` | `-d` | Directory for PGlite storage |
+| `--postgres-url URL` | `-D` | PostgreSQL connection URL |
+| `--no-migrate` | `-M` | Skip automatic migrations |
+| `--help` | | Show help |
+| `--version` | | Show version |
+
+`--pglite-data-path` and `--postgres-url` are mutually exclusive. One of them
+must be provided.
diff --git a/packages/drfed/bin/drfed-server.mjs b/packages/drfed/bin/drfed-server.mjs
new file mode 100644
index 0000000..4dc320d
--- /dev/null
+++ b/packages/drfed/bin/drfed-server.mjs
@@ -0,0 +1,19 @@
+#!/usr/bin/env node
+// DrFed: A web-based platform for developing and debugging ActivityPub apps
+// Copyright (C) 2026 DrFed team
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see .
+import { main } from "../dist/index.mjs";
+
+await main();
diff --git a/packages/drfed/package.json b/packages/drfed/package.json
new file mode 100644
index 0000000..ea51029
--- /dev/null
+++ b/packages/drfed/package.json
@@ -0,0 +1,75 @@
+{
+ "name": "@drfed/drfed",
+ "version": "0.1.0",
+ "description": "The main entrypoint program for DrFed.",
+ "keywords": [
+ "ActivityPub",
+ "fediverse",
+ "federation",
+ "debugger"
+ ],
+ "author": {
+ "name": "DrFed team",
+ "url": "https://drfed.org/"
+ },
+ "maintainers": [
+ {
+ "name": "ChanHaeng Lee",
+ "email": "2chanhaeng@gmail.com",
+ "url": "https://chomu.dev/"
+ },
+ {
+ "name": "Hong Minhee",
+ "email": "hong@minhee.org",
+ "url": "https://hongminhee.org/"
+ },
+ {
+ "name": "Hyeonseo Kim",
+ "email": "dodok8@gmail.com",
+ "url": "https://hackers.pub/@gaebalgom"
+ },
+ {
+ "name": "Jiwon Kwon",
+ "email": "me@kwonjiwon.org",
+ "url": "https://kwonjiwon.org/"
+ }
+ ],
+ "license": "AGPL-3.0-only",
+ "engine": {
+ "node": ">=26.0.0"
+ },
+ "type": "module",
+ "main": "dist/index.mjs",
+ "types": "dist/index.d.mts",
+ "files": [
+ "bin/",
+ "dist/",
+ "README.md"
+ ],
+ "bin": {
+ "drfed-server": "bin/drfed-server.mjs"
+ },
+ "tsdown": {
+ "dts": true,
+ "sourcemap": true
+ },
+ "scripts": {
+ "build": "tsdown"
+ },
+ "devDependencies": {
+ "@types/node": "catalog:",
+ "@types/pg": "^8.20.0",
+ "tsdown": "catalog:",
+ "typescript": "catalog:"
+ },
+ "dependencies": {
+ "@drfed/graphql": "workspace:*",
+ "@drfed/models": "workspace:*",
+ "@electric-sql/pglite": "^0.5.3",
+ "@optique/core": "^1.1.0",
+ "@optique/run": "^1.1.0",
+ "drizzle-orm": "catalog:",
+ "pg": "^8.21.0",
+ "srvx": "^0.11.16"
+ }
+}
diff --git a/packages/drfed/src/index.ts b/packages/drfed/src/index.ts
new file mode 100644
index 0000000..5c938f1
--- /dev/null
+++ b/packages/drfed/src/index.ts
@@ -0,0 +1,53 @@
+// DrFed: A web-based platform for developing and debugging ActivityPub apps
+// Copyright (C) 2026 DrFed team
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see .
+import process from "node:process";
+
+import { createYogaServer } from "@drfed/graphql";
+import { migrate } from "@drfed/models";
+import { run } from "@optique/run";
+import { serve } from "srvx";
+
+import metadata from "../package.json" with { type: "json" };
+import type { Options } from "./parser.ts";
+import program from "./program.ts";
+
+export async function main() {
+ const options: Options = run(program, {
+ help: "option",
+ version: {
+ value: metadata.version,
+ option: true,
+ },
+ showChoices: true,
+ showDefault: true,
+ });
+ if (options.drizzle.migrate) {
+ await migrate({ credentials: options.drizzle.credentials });
+ }
+ const yogaServer = createYogaServer(options.drizzle.db);
+ const server = serve({
+ hostname: options.address.host,
+ port: options.address.port,
+ manual: true,
+ fetch: yogaServer.fetch.bind(yogaServer),
+ });
+ const shutdown = () => {
+ server.close().then(() => process.exit(0));
+ };
+ process.once("SIGINT", shutdown);
+ process.once("SIGTERM", shutdown);
+ await server.serve();
+}
diff --git a/packages/drfed/src/parser.ts b/packages/drfed/src/parser.ts
new file mode 100644
index 0000000..0a84674
--- /dev/null
+++ b/packages/drfed/src/parser.ts
@@ -0,0 +1,99 @@
+// DrFed: A web-based platform for developing and debugging ActivityPub apps
+// Copyright (C) 2026 DrFed team
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see .
+import { relations, schema } from "@drfed/models";
+import { merge, object, or } from "@optique/core/constructs";
+import { message, optionNames } from "@optique/core/message";
+import { map, withDefault } from "@optique/core/modifiers";
+import type { InferValue } from "@optique/core/parser";
+import { option } from "@optique/core/primitives";
+import { socketAddress, url } from "@optique/core/valueparser";
+import { path } from "@optique/run/valueparser";
+import { drizzle as drizzlePostgres } from "drizzle-orm/node-postgres";
+import { drizzle as drizzlePglite } from "drizzle-orm/pglite";
+
+const pgliteParser = map(
+ option(
+ "--pglite-data-path",
+ "--data-path",
+ "-d",
+ path({ type: "directory" }),
+ {
+ description: message`The path to the directory where the PGlite database files will be stored. Mutually exclusive with ${optionNames(["--postgres-url", "--database-url", "-D"])}.`,
+ },
+ ),
+ (path) => ({
+ db: drizzlePglite({
+ schema,
+ relations,
+ connection: { dataDir: path },
+ }),
+ credentials: {
+ driver: "pglite" as const,
+ url: path,
+ },
+ }),
+);
+
+const postgresParser = map(
+ option(
+ "--postgres-url",
+ "--database-url",
+ "-D",
+ url({ allowedProtocols: ["postgres:", "postgresql:"] }),
+ {
+ description: message`The URL of the PostgreSQL database to connect to. Mutually exclusive with ${optionNames(["--pglite-data-path", "--data-path", "-d"])}.`,
+ },
+ ),
+ (url) => ({
+ db: drizzlePostgres({
+ schema,
+ relations,
+ connection: {
+ connectionString: url.href,
+ },
+ }),
+ credentials: {
+ url: url.href,
+ },
+ }),
+);
+
+export const parser = object({
+ address: withDefault(
+ option("--listen", "-l", socketAddress({ requirePort: true }), {
+ description: message`The address to listen on.`,
+ }),
+ {
+ host: "localhost",
+ port: 8888,
+ },
+ ),
+ drizzle: merge(
+ or(pgliteParser, postgresParser),
+ object({
+ migrate: map(
+ option("--no-migrate", "-M", {
+ description: message`Disable automatic database migrations.`,
+ }),
+ (m) => !m,
+ ),
+ }),
+ ),
+});
+
+export type Options = InferValue;
+
+export default parser;
diff --git a/packages/drfed/src/program.ts b/packages/drfed/src/program.ts
new file mode 100644
index 0000000..064a9d2
--- /dev/null
+++ b/packages/drfed/src/program.ts
@@ -0,0 +1,30 @@
+// DrFed: A web-based platform for developing and debugging ActivityPub apps
+// Copyright (C) 2026 DrFed team
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see .
+import { message } from "@optique/core/message";
+import type { InferValue } from "@optique/core/parser";
+import type { Program } from "@optique/core/program";
+
+import parser from "./parser.ts";
+
+const program: Program<"sync", InferValue> = {
+ parser,
+ metadata: {
+ name: "drfed-server",
+ description: message`Run a DrFed server.`,
+ },
+};
+
+export default program;
diff --git a/packages/drfed/tsconfig.json b/packages/drfed/tsconfig.json
new file mode 100644
index 0000000..79bbafc
--- /dev/null
+++ b/packages/drfed/tsconfig.json
@@ -0,0 +1,27 @@
+{
+ "compilerOptions": {
+ "allowImportingTsExtensions": true,
+ "exactOptionalPropertyTypes": true,
+ "forceConsistentCasingInFileNames": true,
+ "isolatedModules": true,
+ "lib": ["ES2024"],
+ "module": "NodeNext",
+ "moduleResolution": "NodeNext",
+ "noEmit": true,
+ "noFallthroughCasesInSwitch": true,
+ "noImplicitOverride": true,
+ "noImplicitReturns": true,
+ "noUncheckedIndexedAccess": true,
+ "paths": {
+ "@drfed/graphql": ["../graphql/src/index.ts"],
+ "@drfed/models": ["../models/src/index.ts"]
+ },
+ "resolveJsonModule": true,
+ "skipLibCheck": true,
+ "strict": true,
+ "target": "ES2024",
+ "types": ["node"],
+ "verbatimModuleSyntax": true
+ },
+ "include": ["src/**/*.ts"]
+}
diff --git a/packages/graphql/README.md b/packages/graphql/README.md
new file mode 100644
index 0000000..f41999c
--- /dev/null
+++ b/packages/graphql/README.md
@@ -0,0 +1,33 @@
+@drfed/graphql
+==============
+
+GraphQL server for [DrFed], built with [Pothos] and [GraphQL Yoga]. Exposes
+a Relay-compatible schema backed by Drizzle ORM.
+
+[DrFed]: https://drfed.org/
+[Pothos]: https://pothos-graphql.dev/
+[GraphQL Yoga]: https://the-guild.dev/graphql/yoga-server
+
+
+Scalars
+-------
+
+| Scalar | Description |
+| ---------- | ------------------------- |
+| `DateTime` | ISO 8601 timestamp |
+| `Email` | Normalized e-mail address |
+| `UUID` | RFC 4122 UUID |
+
+
+Usage
+-----
+
+~~~~ ts
+import { createYogaServer } from "@drfed/graphql";
+
+const yoga = createYogaServer(db);
+serve({ fetch: yoga.fetch.bind(yoga) });
+~~~~
+
+`createYogaServer` accepts a Drizzle database instance and returns a
+GraphQL Yoga server ready to handle HTTP requests.
diff --git a/packages/graphql/dist/index.mjs b/packages/graphql/dist/index.mjs
deleted file mode 100644
index cb0ff5c..0000000
--- a/packages/graphql/dist/index.mjs
+++ /dev/null
@@ -1 +0,0 @@
-export {};
diff --git a/packages/graphql/package.json b/packages/graphql/package.json
index 09d1fb2..7019fad 100644
--- a/packages/graphql/package.json
+++ b/packages/graphql/package.json
@@ -35,12 +35,36 @@
}
],
"license": "AGPL-3.0-only",
+ "engine": {
+ "node": ">=26.0.0"
+ },
"type": "module",
"main": "dist/index.mjs",
+ "types": "dist/index.d.mts",
+ "files": [
+ "dist/",
+ "README.md"
+ ],
+ "tsdown": {
+ "dts": true,
+ "sourcemap": true
+ },
"scripts": {
"build": "tsdown"
},
"devDependencies": {
- "tsdown": "catalog:"
+ "@types/node": "catalog:",
+ "tsdown": "catalog:",
+ "typescript": "catalog:"
+ },
+ "dependencies": {
+ "@drfed/models": "workspace:*",
+ "@pothos/core": "^4.13.0",
+ "@pothos/plugin-drizzle": "^0.17.4",
+ "@pothos/plugin-relay": "^4.7.0",
+ "drizzle-orm": "catalog:",
+ "graphql": "^16.14.2",
+ "graphql-scalars": "^1.25.0",
+ "graphql-yoga": "^5.21.2"
}
}
diff --git a/packages/graphql/src/account.ts b/packages/graphql/src/account.ts
new file mode 100644
index 0000000..36d4f8a
--- /dev/null
+++ b/packages/graphql/src/account.ts
@@ -0,0 +1,61 @@
+// DrFed: A web-based platform for developing and debugging ActivityPub apps
+// Copyright (C) 2026 DrFed team
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see .
+import builder from "./builder.ts";
+
+export const Account = builder.drizzleNode("accounts", {
+ name: "Account",
+ description:
+ "Represents an `Account` in the DrFed platform. " +
+ "Note that it differs from the ActivityPub `Actor`s that belong to `Instance`s.",
+ id: {
+ column(account) {
+ return account.id;
+ },
+ description: "The unique identifier of the `Account`.",
+ },
+ fields: (t) => ({
+ uuid: t.expose("id", {
+ type: "UUID",
+ description: "The UUID of the `Account`.",
+ }),
+ email: t.expose("email", {
+ type: "Email",
+ description: "The email address of the `Account`.",
+ }),
+ created: t.expose("created", {
+ type: "DateTime",
+ description: "The date/time when the `Account` was created.",
+ }),
+ }),
+});
+
+builder.queryFields((t) => ({
+ accountByUuid: t.drizzleField({
+ type: Account,
+ description: "Get an `Account` by its UUID.",
+ args: {
+ uuid: t.arg({
+ type: "UUID",
+ required: true,
+ description: "The UUID of the `Account` to retrieve.",
+ }),
+ },
+ nullable: true,
+ resolve(query, _, { uuid }, ctx) {
+ return ctx.db.query.accounts.findFirst(query({ where: { id: uuid } }));
+ },
+ }),
+}));
diff --git a/packages/graphql/src/builder.ts b/packages/graphql/src/builder.ts
new file mode 100644
index 0000000..62df5f6
--- /dev/null
+++ b/packages/graphql/src/builder.ts
@@ -0,0 +1,92 @@
+// DrFed: A web-based platform for developing and debugging ActivityPub apps
+// Copyright (C) 2026 DrFed team
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see .
+import { type Database, normalizeEmail, relations } from "@drfed/models";
+import SchemaBuilder from "@pothos/core";
+import DrizzlePlugin from "@pothos/plugin-drizzle";
+import RelayPlugin from "@pothos/plugin-relay";
+import { getTableConfig } from "drizzle-orm/pg-core";
+import { DateTimeResolver, UUIDResolver } from "graphql-scalars";
+
+/**
+ * The context data for the GraphQL server, which includes the incoming request
+ * object and any additional information needed for processing GraphQL queries
+ * and mutations.
+ */
+export interface ServerContext {
+ /**
+ * The incoming HTTP request.
+ */
+ readonly request: Request;
+
+ /**
+ * The database instance.
+ */
+ readonly db: Database;
+}
+
+/**
+ * The user-related context data for the GraphQL server, which include every
+ * field from the {@link ServerContext}.
+ */
+export interface UserContext extends ServerContext {}
+
+export interface SchemaTypes {
+ Context: UserContext;
+ Scalars: {
+ DateTime: {
+ Input: Date;
+ Output: Date;
+ };
+ Email: {
+ Input: string;
+ Output: string;
+ };
+ UUID: {
+ Input: string;
+ Output: string;
+ };
+ };
+ DefaultFieldNullability: false;
+ DrizzleRelations: typeof relations;
+}
+
+/**
+ * The GraphQL schema builder.
+ */
+export const builder = new SchemaBuilder({
+ plugins: [DrizzlePlugin, RelayPlugin],
+ defaultFieldNullability: false,
+ drizzle: {
+ client(ctx) {
+ return ctx.db;
+ },
+ getTableConfig,
+ relations,
+ },
+});
+
+builder.addScalarType("DateTime", DateTimeResolver);
+
+builder.scalarType("Email", {
+ serialize: (v) => normalizeEmail(v),
+ parseValue: (v) => normalizeEmail(String(v)),
+});
+
+builder.addScalarType("UUID", UUIDResolver);
+
+export const Node = builder.nodeInterfaceRef();
+
+export default builder;
diff --git a/packages/graphql/src/index.ts b/packages/graphql/src/index.ts
index c7f8f2f..81d87ff 100644
--- a/packages/graphql/src/index.ts
+++ b/packages/graphql/src/index.ts
@@ -13,3 +13,27 @@
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see .
+import type { Database } from "@drfed/models";
+import { createYoga, useExecutionCancellation } from "graphql-yoga";
+import type { YogaServerInstance } from "graphql-yoga";
+
+import type { ServerContext, UserContext } from "./builder.ts";
+import { schema } from "./schema.ts";
+
+/**
+ * Creates a Yoga server instance with the provided schema and context.
+ * @param db The database instance.
+ * @returns A `YogaServerInstance` configured with the schema and context for
+ * handling GraphQL requests.
+ */
+export function createYogaServer(
+ db: Database,
+): YogaServerInstance {
+ return createYoga({
+ plugins: [useExecutionCancellation()],
+ schema,
+ async context(ctx) {
+ return { request: ctx.request, db };
+ },
+ });
+}
diff --git a/packages/graphql/src/instance.ts b/packages/graphql/src/instance.ts
new file mode 100644
index 0000000..e73bd5c
--- /dev/null
+++ b/packages/graphql/src/instance.ts
@@ -0,0 +1,38 @@
+// DrFed: A web-based platform for developing and debugging ActivityPub apps
+// Copyright (C) 2026 DrFed team
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see .
+import builder from "./builder.ts";
+
+export const Instance = builder.drizzleNode("instances", {
+ name: "Instance",
+ description: "Represents an `Instance` in the DrFed platform.",
+ id: {
+ column(instance) {
+ return instance.id;
+ },
+ description: "The unique identifier of the `Instance`.",
+ },
+ fields: (t) => ({
+ slug: t.exposeString("slug"),
+ expires: t.expose("expires", {
+ type: "DateTime",
+ description: "The expiration date/time of the `Instance`.",
+ }),
+ created: t.expose("created", {
+ type: "DateTime",
+ description: "The creation date/time of the `Instance`.",
+ }),
+ }),
+});
diff --git a/packages/graphql/src/schema.ts b/packages/graphql/src/schema.ts
new file mode 100644
index 0000000..7acc9e1
--- /dev/null
+++ b/packages/graphql/src/schema.ts
@@ -0,0 +1,23 @@
+// DrFed: A web-based platform for developing and debugging ActivityPub apps
+// Copyright (C) 2026 DrFed team
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see .
+import "./account.ts";
+import "./instance.ts";
+import builder from "./builder.ts";
+
+builder.queryType({});
+// builder.mutationType({});
+
+export const schema = builder.toSchema();
diff --git a/packages/graphql/tsconfig.json b/packages/graphql/tsconfig.json
new file mode 100644
index 0000000..4f8dc42
--- /dev/null
+++ b/packages/graphql/tsconfig.json
@@ -0,0 +1,25 @@
+{
+ "compilerOptions": {
+ "allowImportingTsExtensions": true,
+ "exactOptionalPropertyTypes": true,
+ "forceConsistentCasingInFileNames": true,
+ "isolatedModules": true,
+ "lib": ["ES2024"],
+ "module": "NodeNext",
+ "moduleResolution": "NodeNext",
+ "noEmit": true,
+ "noFallthroughCasesInSwitch": true,
+ "noImplicitOverride": true,
+ "noImplicitReturns": true,
+ "noUncheckedIndexedAccess": true,
+ "paths": {
+ "@drfed/models": ["../models/src/index.ts"]
+ },
+ "skipLibCheck": true,
+ "strict": true,
+ "target": "ES2024",
+ "types": ["node"],
+ "verbatimModuleSyntax": true
+ },
+ "include": ["src/**/*.ts"]
+}
diff --git a/packages/models/README.md b/packages/models/README.md
new file mode 100644
index 0000000..645949a
--- /dev/null
+++ b/packages/models/README.md
@@ -0,0 +1,30 @@
+@drfed/models
+=============
+
+Database schema, Drizzle ORM types, and migration runner for [DrFed]. Supports
+both PGlite (embedded) and PostgreSQL.
+
+[DrFed]: https://drfed.org/
+
+
+Schema
+------
+
+| Table | Key columns |
+| ------------------ | ---------------------------------------------------- |
+| `accounts` | `id` (UUID), `email`, `created` |
+| `instances` | `id` (UUID), `slug`, `expires`, `created` |
+| `instance_members` | `instanceId` → `instances`, `accountId` → `accounts` |
+
+
+Migrations
+----------
+
+Generate a migration after changing *src/schema.ts*:
+
+~~~~ sh
+mise run generate:migrate --name your_migration_name
+~~~~
+
+The *drizzle/* directory is included in the published npm package so that
+installed users can run migrations without access to the repository source.
diff --git a/packages/models/drizzle.config.ts b/packages/models/drizzle.config.ts
new file mode 100644
index 0000000..f66da70
--- /dev/null
+++ b/packages/models/drizzle.config.ts
@@ -0,0 +1,21 @@
+// DrFed: A web-based platform for developing and debugging ActivityPub apps
+// Copyright (C) 2026 DrFed team
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see .
+import { defineConfig } from "drizzle-kit";
+
+export default defineConfig({
+ dialect: "postgresql",
+ schema: "./src/schema.ts",
+});
diff --git a/packages/models/drizzle/20260620011017_init/migration.sql b/packages/models/drizzle/20260620011017_init/migration.sql
new file mode 100644
index 0000000..ed111aa
--- /dev/null
+++ b/packages/models/drizzle/20260620011017_init/migration.sql
@@ -0,0 +1,24 @@
+CREATE TABLE "accounts" (
+ "id" uuid PRIMARY KEY,
+ "email" varchar(255) NOT NULL UNIQUE,
+ "created" timestamp with time zone DEFAULT CURRENT_TIMESTAMP NOT NULL,
+ CONSTRAINT "accounts_email_check" CHECK ("email" ~ '^[^@]+@[^@]+\.[^@]+$')
+);
+--> statement-breakpoint
+CREATE TABLE "instance_members" (
+ "instanceId" uuid PRIMARY KEY,
+ "accountId" uuid NOT NULL,
+ "created" timestamp with time zone DEFAULT CURRENT_TIMESTAMP NOT NULL
+);
+--> statement-breakpoint
+CREATE TABLE "instances" (
+ "id" uuid PRIMARY KEY,
+ "slug" varchar(100) NOT NULL UNIQUE,
+ "expires" timestamp with time zone NOT NULL,
+ "created" timestamp with time zone DEFAULT CURRENT_TIMESTAMP NOT NULL,
+ CONSTRAINT "instances_slug_check" CHECK ("slug" ~ '^[a-z0-9-]{4,100}$'),
+ CONSTRAINT "instances_expires_check" CHECK ("expires" < ("created" + INTERVAL '1 year'))
+);
+--> statement-breakpoint
+ALTER TABLE "instance_members" ADD CONSTRAINT "instance_members_instanceId_instances_id_fkey" FOREIGN KEY ("instanceId") REFERENCES "instances"("id");--> statement-breakpoint
+ALTER TABLE "instance_members" ADD CONSTRAINT "instance_members_accountId_accounts_id_fkey" FOREIGN KEY ("accountId") REFERENCES "accounts"("id");
\ No newline at end of file
diff --git a/packages/models/drizzle/20260620011017_init/snapshot.json b/packages/models/drizzle/20260620011017_init/snapshot.json
new file mode 100644
index 0000000..a65d9e3
--- /dev/null
+++ b/packages/models/drizzle/20260620011017_init/snapshot.json
@@ -0,0 +1,246 @@
+{
+ "version": "8",
+ "dialect": "postgres",
+ "id": "e399ae79-2d5c-4f12-b5e7-8d602703fcc1",
+ "prevIds": ["00000000-0000-0000-0000-000000000000"],
+ "ddl": [
+ {
+ "isRlsEnabled": false,
+ "name": "accounts",
+ "entityType": "tables",
+ "schema": "public"
+ },
+ {
+ "isRlsEnabled": false,
+ "name": "instance_members",
+ "entityType": "tables",
+ "schema": "public"
+ },
+ {
+ "isRlsEnabled": false,
+ "name": "instances",
+ "entityType": "tables",
+ "schema": "public"
+ },
+ {
+ "type": "uuid",
+ "typeSchema": null,
+ "notNull": true,
+ "dimensions": 0,
+ "default": null,
+ "generated": null,
+ "identity": null,
+ "name": "id",
+ "entityType": "columns",
+ "schema": "public",
+ "table": "accounts"
+ },
+ {
+ "type": "varchar(255)",
+ "typeSchema": null,
+ "notNull": true,
+ "dimensions": 0,
+ "default": null,
+ "generated": null,
+ "identity": null,
+ "name": "email",
+ "entityType": "columns",
+ "schema": "public",
+ "table": "accounts"
+ },
+ {
+ "type": "timestamp with time zone",
+ "typeSchema": null,
+ "notNull": true,
+ "dimensions": 0,
+ "default": "CURRENT_TIMESTAMP",
+ "generated": null,
+ "identity": null,
+ "name": "created",
+ "entityType": "columns",
+ "schema": "public",
+ "table": "accounts"
+ },
+ {
+ "type": "uuid",
+ "typeSchema": null,
+ "notNull": true,
+ "dimensions": 0,
+ "default": null,
+ "generated": null,
+ "identity": null,
+ "name": "instanceId",
+ "entityType": "columns",
+ "schema": "public",
+ "table": "instance_members"
+ },
+ {
+ "type": "uuid",
+ "typeSchema": null,
+ "notNull": true,
+ "dimensions": 0,
+ "default": null,
+ "generated": null,
+ "identity": null,
+ "name": "accountId",
+ "entityType": "columns",
+ "schema": "public",
+ "table": "instance_members"
+ },
+ {
+ "type": "timestamp with time zone",
+ "typeSchema": null,
+ "notNull": true,
+ "dimensions": 0,
+ "default": "CURRENT_TIMESTAMP",
+ "generated": null,
+ "identity": null,
+ "name": "created",
+ "entityType": "columns",
+ "schema": "public",
+ "table": "instance_members"
+ },
+ {
+ "type": "uuid",
+ "typeSchema": null,
+ "notNull": true,
+ "dimensions": 0,
+ "default": null,
+ "generated": null,
+ "identity": null,
+ "name": "id",
+ "entityType": "columns",
+ "schema": "public",
+ "table": "instances"
+ },
+ {
+ "type": "varchar(100)",
+ "typeSchema": null,
+ "notNull": true,
+ "dimensions": 0,
+ "default": null,
+ "generated": null,
+ "identity": null,
+ "name": "slug",
+ "entityType": "columns",
+ "schema": "public",
+ "table": "instances"
+ },
+ {
+ "type": "timestamp with time zone",
+ "typeSchema": null,
+ "notNull": true,
+ "dimensions": 0,
+ "default": null,
+ "generated": null,
+ "identity": null,
+ "name": "expires",
+ "entityType": "columns",
+ "schema": "public",
+ "table": "instances"
+ },
+ {
+ "type": "timestamp with time zone",
+ "typeSchema": null,
+ "notNull": true,
+ "dimensions": 0,
+ "default": "CURRENT_TIMESTAMP",
+ "generated": null,
+ "identity": null,
+ "name": "created",
+ "entityType": "columns",
+ "schema": "public",
+ "table": "instances"
+ },
+ {
+ "nameExplicit": false,
+ "columns": ["instanceId"],
+ "schemaTo": "public",
+ "tableTo": "instances",
+ "columnsTo": ["id"],
+ "onUpdate": "NO ACTION",
+ "onDelete": "NO ACTION",
+ "name": "instance_members_instanceId_instances_id_fkey",
+ "entityType": "fks",
+ "schema": "public",
+ "table": "instance_members"
+ },
+ {
+ "nameExplicit": false,
+ "columns": ["accountId"],
+ "schemaTo": "public",
+ "tableTo": "accounts",
+ "columnsTo": ["id"],
+ "onUpdate": "NO ACTION",
+ "onDelete": "NO ACTION",
+ "name": "instance_members_accountId_accounts_id_fkey",
+ "entityType": "fks",
+ "schema": "public",
+ "table": "instance_members"
+ },
+ {
+ "columns": ["id"],
+ "nameExplicit": false,
+ "name": "accounts_pkey",
+ "schema": "public",
+ "table": "accounts",
+ "entityType": "pks"
+ },
+ {
+ "columns": ["instanceId"],
+ "nameExplicit": false,
+ "name": "instance_members_pkey",
+ "schema": "public",
+ "table": "instance_members",
+ "entityType": "pks"
+ },
+ {
+ "columns": ["id"],
+ "nameExplicit": false,
+ "name": "instances_pkey",
+ "schema": "public",
+ "table": "instances",
+ "entityType": "pks"
+ },
+ {
+ "nameExplicit": false,
+ "columns": ["email"],
+ "nullsNotDistinct": false,
+ "name": "accounts_email_key",
+ "schema": "public",
+ "table": "accounts",
+ "entityType": "uniques"
+ },
+ {
+ "nameExplicit": false,
+ "columns": ["slug"],
+ "nullsNotDistinct": false,
+ "name": "instances_slug_key",
+ "schema": "public",
+ "table": "instances",
+ "entityType": "uniques"
+ },
+ {
+ "value": "\"email\" ~ '^[^@]+@[^@]+\\.[^@]+$'",
+ "name": "accounts_email_check",
+ "entityType": "checks",
+ "schema": "public",
+ "table": "accounts"
+ },
+ {
+ "value": "\"slug\" ~ '^[a-z0-9-]{4,100}$'",
+ "name": "instances_slug_check",
+ "entityType": "checks",
+ "schema": "public",
+ "table": "instances"
+ },
+ {
+ "value": "\"expires\" < (\"created\" + INTERVAL '1 year')",
+ "name": "instances_expires_check",
+ "entityType": "checks",
+ "schema": "public",
+ "table": "instances"
+ }
+ ],
+ "renames": []
+}
diff --git a/packages/models/package.json b/packages/models/package.json
new file mode 100644
index 0000000..553de0b
--- /dev/null
+++ b/packages/models/package.json
@@ -0,0 +1,68 @@
+{
+ "name": "@drfed/models",
+ "version": "0.1.0",
+ "description": "Database models for DrFed.",
+ "keywords": [
+ "ActivityPub",
+ "fediverse",
+ "federation",
+ "debugger"
+ ],
+ "author": {
+ "name": "DrFed team",
+ "url": "https://drfed.org/"
+ },
+ "maintainers": [
+ {
+ "name": "ChanHaeng Lee",
+ "email": "2chanhaeng@gmail.com",
+ "url": "https://chomu.dev/"
+ },
+ {
+ "name": "Hong Minhee",
+ "email": "hong@minhee.org",
+ "url": "https://hongminhee.org/"
+ },
+ {
+ "name": "Hyeonseo Kim",
+ "email": "dodok8@gmail.com",
+ "url": "https://hackers.pub/@gaebalgom"
+ },
+ {
+ "name": "Jiwon Kwon",
+ "email": "me@kwonjiwon.org",
+ "url": "https://kwonjiwon.org/"
+ }
+ ],
+ "license": "AGPL-3.0-only",
+ "engine": {
+ "node": ">=26.0.0"
+ },
+ "type": "module",
+ "main": "dist/index.mjs",
+ "types": "dist/index.d.mts",
+ "files": [
+ "dist/",
+ "drizzle/",
+ "README.md"
+ ],
+ "tsdown": {
+ "dts": true,
+ "sourcemap": true
+ },
+ "scripts": {
+ "build": "tsdown"
+ },
+ "devDependencies": {
+ "@types/node": "catalog:",
+ "@types/pg": "^8.20.0",
+ "drizzle-kit": "1.0.0-beta.22",
+ "tsdown": "catalog:",
+ "typescript": "catalog:"
+ },
+ "dependencies": {
+ "@electric-sql/pglite": "^0.5.3",
+ "drizzle-orm": "catalog:",
+ "pg": "^8.21.0"
+ }
+}
diff --git a/packages/models/src/db.ts b/packages/models/src/db.ts
new file mode 100644
index 0000000..c033ebb
--- /dev/null
+++ b/packages/models/src/db.ts
@@ -0,0 +1,48 @@
+// DrFed: A web-based platform for developing and debugging ActivityPub apps
+// Copyright (C) 2026 DrFed team
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see .
+import type { RelationsFilter as RelationsFilterImpl } from "drizzle-orm";
+import type {
+ PgAsyncDatabase,
+ PgAsyncTransaction,
+ PgQueryResultHKT,
+} from "drizzle-orm/pg-core";
+
+import { relations } from "./relations.ts";
+import * as schema from "./schema.ts";
+
+/**
+ * A database instance.
+ */
+export type Database = PgAsyncDatabase<
+ PgQueryResultHKT,
+ typeof schema,
+ typeof relations
+>;
+
+/**
+ * A transaction instance.
+ */
+export type Transaction = PgAsyncTransaction<
+ PgQueryResultHKT,
+ typeof schema,
+ typeof relations
+>;
+
+/**
+ * A filter for relations.
+ */
+export type RelationsFilter =
+ RelationsFilterImpl<(typeof relations)[T], typeof relations>;
diff --git a/packages/models/src/email.ts b/packages/models/src/email.ts
new file mode 100644
index 0000000..2ab6894
--- /dev/null
+++ b/packages/models/src/email.ts
@@ -0,0 +1,75 @@
+// DrFed: A web-based platform for developing and debugging ActivityPub apps
+// Copyright (C) 2026 DrFed team
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see .
+
+/**
+ * Normalizes an email address by trimming whitespace, converting the host
+ * to lowercase (and to punycode if necessary), and ensuring it has a
+ * single "@" character. If the email is invalid, it throws a `TypeError`.
+ * @param email The email address to normalize.
+ * @returns The normalized email address.
+ */
+export function normalizeEmail(email: string): string;
+
+/**
+ * Normalizes an email address by trimming whitespace, converting the host
+ * to lowercase (and to punycode if necessary), and ensuring it has a
+ * single "@" character. If the email is invalid, it throws a `TypeError`.
+ * @param email The email address to normalize.
+ * @returns The normalized email address. If the input is `null`,
+ * it returns `null`.
+ */
+export function normalizeEmail(email: string | null): string | null;
+
+/**
+ * Normalizes an email address by trimming whitespace, converting the host
+ * to lowercase (and to punycode if necessary), and ensuring it has a
+ * single "@" character. If the email is invalid, it throws a `TypeError`.
+ * @param email The email address to normalize.
+ * @returns The normalized email address. If the input is `undefined`,
+ * it returns `undefined`.
+ */
+export function normalizeEmail(email: string | undefined): string | undefined;
+
+/**
+ * Normalizes an email address by trimming whitespace, converting the host
+ * to lowercase (and to punycode if necessary), and ensuring it has a
+ * single "@" character. If the email is invalid, it throws a `TypeError`.
+ * @param email The email address to normalize.
+ * @returns The normalized email address. If the input is `undefined`,
+ * it returns `undefined`. If the input is `null`, it returns `null`.
+ */
+export function normalizeEmail(
+ email: string | null | undefined,
+): string | null | undefined;
+
+export function normalizeEmail(
+ email: string | null | undefined,
+): string | null | undefined {
+ if (typeof email === "undefined") return undefined;
+ else if (email == null) return null;
+ const [local, host, shouldNotExist] = email.trim().split("@");
+ if (
+ local == null ||
+ local.trim() === "" ||
+ host == null ||
+ host.trim() === "" ||
+ shouldNotExist != null
+ ) {
+ throw new TypeError("Invalid email format.");
+ }
+ const normalizedHost = new URL(`https://${host}/`).host;
+ return `${local}@${normalizedHost}`;
+}
diff --git a/packages/models/src/index.ts b/packages/models/src/index.ts
new file mode 100644
index 0000000..b69235a
--- /dev/null
+++ b/packages/models/src/index.ts
@@ -0,0 +1,20 @@
+// DrFed: A web-based platform for developing and debugging ActivityPub apps
+// Copyright (C) 2026 DrFed team
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see .
+export * from "./email.ts";
+export * from "./db.ts";
+export * from "./migrate.ts";
+export { relations } from "./relations.ts";
+export * as schema from "./schema.ts";
diff --git a/packages/models/src/migrate.ts b/packages/models/src/migrate.ts
new file mode 100644
index 0000000..a1b753b
--- /dev/null
+++ b/packages/models/src/migrate.ts
@@ -0,0 +1,169 @@
+// DrFed: A web-based platform for developing and debugging ActivityPub apps
+// Copyright (C) 2026 DrFed team
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see .
+import { existsSync } from "node:fs";
+import { dirname, join } from "node:path";
+import { fileURLToPath } from "node:url";
+
+import { PGlite, type PGliteOptions } from "@electric-sql/pglite";
+import { drizzle as drizzlePostgres } from "drizzle-orm/node-postgres";
+import { migrate as migratePostgres } from "drizzle-orm/node-postgres/migrator";
+import { drizzle as drizzlePglite } from "drizzle-orm/pglite";
+import { migrate as migratePglite } from "drizzle-orm/pglite/migrator";
+import { Pool, type PoolConfig } from "pg";
+
+export type MigrateCredentials =
+ | PostgresMigrateCredentials
+ | PGliteMigrateCredentials;
+
+export type PostgresMigrateCredentials =
+ | (Omit & {
+ readonly url: string;
+ readonly driver?: never;
+ })
+ | (PoolConfig & {
+ readonly url?: never;
+ readonly driver?: never;
+ });
+
+export type PGliteMigrateCredentials =
+ | {
+ readonly driver: "pglite";
+ readonly url: string;
+ readonly options?: PGliteOptions;
+ }
+ | {
+ readonly driver: "pglite";
+ readonly client: PGlite;
+ };
+
+export interface MigrateOptions {
+ /**
+ * PostgreSQL or PGlite credentials.
+ */
+ readonly credentials: MigrateCredentials;
+
+ /**
+ * Custom migrations table/schema. Equivalent to drizzle-kit config's
+ * `migrations` option.
+ */
+ readonly migrations?: {
+ readonly table?: string;
+ readonly schema?: string;
+ };
+
+ /**
+ * Custom migrations table. Takes precedence over `migrations.table`.
+ */
+ readonly migrationsTable?: string;
+
+ /**
+ * Custom migrations schema. Takes precedence over `migrations.schema`.
+ */
+ readonly migrationsSchema?: string;
+}
+
+const migrationsFolder = join(
+ dirname(fileURLToPath(import.meta.url)),
+ "..",
+ "drizzle",
+);
+
+export async function migrate(options: MigrateOptions): Promise {
+ assertV3MigrationsFolder(migrationsFolder);
+
+ const config = {
+ migrationsFolder,
+ migrationsTable:
+ options.migrationsTable ??
+ options.migrations?.table ??
+ "__drizzle_migrations",
+ migrationsSchema:
+ options.migrationsSchema ?? options.migrations?.schema ?? "drizzle",
+ };
+
+ if (isPGliteMigrateCredentials(options.credentials)) {
+ await migratePGliteDatabase(options.credentials, config);
+ } else {
+ await migratePostgresDatabase(options.credentials, config);
+ }
+}
+
+function assertV3MigrationsFolder(migrationsFolder: string): void {
+ if (!existsSync(join(migrationsFolder, "meta", "_journal.json"))) return;
+
+ throw new Error(
+ `The migrations folder format is outdated: ${migrationsFolder}. ` +
+ "Run `drizzle-kit up` before using migrate().",
+ );
+}
+
+function isPGliteMigrateCredentials(
+ credentials: MigrateCredentials,
+): credentials is PGliteMigrateCredentials {
+ return credentials.driver === "pglite";
+}
+
+async function migratePGliteDatabase(
+ credentials: PGliteMigrateCredentials,
+ config: MigrationConfig,
+): Promise {
+ const client =
+ "client" in credentials
+ ? credentials.client
+ : new PGlite(normalizePGliteUrl(credentials.url), credentials.options);
+ const shouldCloseClient = !("client" in credentials);
+
+ try {
+ await client.waitReady;
+ await migratePglite(drizzlePglite({ client }), config);
+ } finally {
+ if (shouldCloseClient) await client.close();
+ }
+}
+
+async function migratePostgresDatabase(
+ credentials: PostgresMigrateCredentials,
+ config: MigrationConfig,
+): Promise {
+ const pool =
+ "url" in credentials
+ ? new Pool({
+ ...credentials,
+ connectionString: credentials.url,
+ max: 1,
+ })
+ : new Pool({
+ ...credentials,
+ max: 1,
+ });
+
+ try {
+ await migratePostgres(drizzlePostgres({ client: pool }), config);
+ } finally {
+ await pool.end();
+ }
+}
+
+function normalizePGliteUrl(url: string): string {
+ if (url.startsWith("file:")) return url.slice("file:".length);
+ return url;
+}
+
+interface MigrationConfig {
+ readonly migrationsFolder: string;
+ readonly migrationsTable: string;
+ readonly migrationsSchema: string;
+}
diff --git a/packages/models/src/relations.ts b/packages/models/src/relations.ts
new file mode 100644
index 0000000..73ef89e
--- /dev/null
+++ b/packages/models/src/relations.ts
@@ -0,0 +1,45 @@
+// DrFed: A web-based platform for developing and debugging ActivityPub apps
+// Copyright (C) 2026 DrFed team
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see .
+import { defineRelations } from "drizzle-orm";
+
+import * as schema from "./schema.ts";
+
+export const relations = defineRelations(schema, (r) => ({
+ accounts: {
+ instances: r.many.instances({
+ from: r.accounts.id.through(r.instanceMembers.accountId),
+ to: r.instances.id.through(r.instanceMembers.instanceId),
+ }),
+ },
+ instances: {
+ members: r.many.accounts({
+ from: r.instances.id.through(r.instanceMembers.instanceId),
+ to: r.accounts.id.through(r.instanceMembers.accountId),
+ }),
+ },
+ instanceMembers: {
+ instance: r.one.instances({
+ from: r.instanceMembers.instanceId,
+ to: r.instances.id,
+ }),
+ account: r.one.accounts({
+ from: r.instanceMembers.accountId,
+ to: r.accounts.id,
+ }),
+ },
+}));
+
+export default relations;
diff --git a/packages/models/src/schema.ts b/packages/models/src/schema.ts
new file mode 100644
index 0000000..2bc4416
--- /dev/null
+++ b/packages/models/src/schema.ts
@@ -0,0 +1,76 @@
+// DrFed: A web-based platform for developing and debugging ActivityPub apps
+// Copyright (C) 2026 DrFed team
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see .
+import { sql } from "drizzle-orm";
+import { uuid, varchar, pgTable, check, timestamp } from "drizzle-orm/pg-core";
+
+/**
+ * The database table to represent accounts.
+ */
+export const accounts = pgTable(
+ "accounts",
+ {
+ id: uuid().primaryKey(),
+ email: varchar({ length: 255 }).notNull().unique(),
+ created: timestamp({ withTimezone: true })
+ .notNull()
+ .default(sql`CURRENT_TIMESTAMP`),
+ },
+ (table) => [
+ check(
+ "accounts_email_check",
+ sql`${table.email} ~ '^[^@]+@[^@]+\\.[^@]+$'`,
+ ),
+ ],
+);
+
+/**
+ * The database table to represent instances.
+ */
+export const instances = pgTable(
+ "instances",
+ {
+ id: uuid().primaryKey(),
+ slug: varchar({ length: 100 }).notNull().unique(),
+ expires: timestamp({ withTimezone: true }).notNull(),
+ created: timestamp({ withTimezone: true })
+ .notNull()
+ .default(sql`CURRENT_TIMESTAMP`),
+ },
+ (table) => [
+ check("instances_slug_check", sql`${table.slug} ~ '^[a-z0-9-]{4,100}$'`),
+ check(
+ "instances_expires_check",
+ sql`${table.expires} < (${table.created} + INTERVAL '1 year')`,
+ ),
+ ],
+);
+
+/**
+ * The association table between instances and its member accounts.
+ */
+export const instanceMembers = pgTable("instance_members", {
+ instanceId: uuid()
+ .notNull()
+ .primaryKey()
+ .references(() => instances.id),
+ accountId: uuid()
+ .notNull()
+ .primaryKey()
+ .references(() => accounts.id),
+ created: timestamp({ withTimezone: true })
+ .notNull()
+ .default(sql`CURRENT_TIMESTAMP`),
+});
diff --git a/packages/models/tsconfig.json b/packages/models/tsconfig.json
new file mode 100644
index 0000000..6e88b43
--- /dev/null
+++ b/packages/models/tsconfig.json
@@ -0,0 +1,22 @@
+{
+ "compilerOptions": {
+ "allowImportingTsExtensions": true,
+ "exactOptionalPropertyTypes": true,
+ "forceConsistentCasingInFileNames": true,
+ "isolatedModules": true,
+ "lib": ["ES2024"],
+ "module": "NodeNext",
+ "moduleResolution": "NodeNext",
+ "noEmit": true,
+ "noFallthroughCasesInSwitch": true,
+ "noImplicitOverride": true,
+ "noImplicitReturns": true,
+ "noUncheckedIndexedAccess": true,
+ "skipLibCheck": true,
+ "strict": true,
+ "target": "ES2024",
+ "types": ["node"],
+ "verbatimModuleSyntax": true
+ },
+ "include": ["src/**/*.ts", "drizzle.config.ts"]
+}
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 07aa6a4..fdcf570 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -6,17 +6,125 @@ settings:
catalogs:
default:
+ '@types/node':
+ specifier: ^26.0.0
+ version: 26.0.0
+ drizzle-orm:
+ specifier: 1.0.0-beta.22
+ version: 1.0.0-beta.22
tsdown:
specifier: ^0.22.3
version: 0.22.3
+ typescript:
+ specifier: ^6.0.3
+ version: 6.0.3
importers:
+ packages/drfed:
+ dependencies:
+ '@drfed/graphql':
+ specifier: workspace:*
+ version: link:../graphql
+ '@drfed/models':
+ specifier: workspace:*
+ version: link:../models
+ '@electric-sql/pglite':
+ specifier: ^0.5.3
+ version: 0.5.3
+ '@optique/core':
+ specifier: ^1.1.0
+ version: 1.1.0
+ '@optique/run':
+ specifier: ^1.1.0
+ version: 1.1.0
+ drizzle-orm:
+ specifier: 'catalog:'
+ version: 1.0.0-beta.22(@electric-sql/pglite@0.5.3)(@types/pg@8.20.0)(pg@8.21.0)
+ pg:
+ specifier: ^8.21.0
+ version: 8.21.0
+ srvx:
+ specifier: ^0.11.16
+ version: 0.11.16
+ devDependencies:
+ '@types/node':
+ specifier: 'catalog:'
+ version: 26.0.0
+ '@types/pg':
+ specifier: ^8.20.0
+ version: 8.20.0
+ tsdown:
+ specifier: 'catalog:'
+ version: 0.22.3(typescript@6.0.3)
+ typescript:
+ specifier: 'catalog:'
+ version: 6.0.3
+
packages/graphql:
+ dependencies:
+ '@drfed/models':
+ specifier: workspace:*
+ version: link:../models
+ '@pothos/core':
+ specifier: ^4.13.0
+ version: 4.13.0(graphql@16.14.2)
+ '@pothos/plugin-drizzle':
+ specifier: ^0.17.4
+ version: 0.17.4(@pothos/core@4.13.0(graphql@16.14.2))(drizzle-orm@1.0.0-beta.22(@electric-sql/pglite@0.5.3)(@types/pg@8.20.0)(pg@8.21.0))(graphql@16.14.2)
+ '@pothos/plugin-relay':
+ specifier: ^4.7.0
+ version: 4.7.0(@pothos/core@4.13.0(graphql@16.14.2))(graphql@16.14.2)
+ drizzle-orm:
+ specifier: 'catalog:'
+ version: 1.0.0-beta.22(@electric-sql/pglite@0.5.3)(@types/pg@8.20.0)(pg@8.21.0)
+ graphql:
+ specifier: ^16.14.2
+ version: 16.14.2
+ graphql-scalars:
+ specifier: ^1.25.0
+ version: 1.25.0(graphql@16.14.2)
+ graphql-yoga:
+ specifier: ^5.21.2
+ version: 5.21.2(graphql@16.14.2)
+ devDependencies:
+ '@types/node':
+ specifier: 'catalog:'
+ version: 26.0.0
+ tsdown:
+ specifier: 'catalog:'
+ version: 0.22.3(typescript@6.0.3)
+ typescript:
+ specifier: 'catalog:'
+ version: 6.0.3
+
+ packages/models:
+ dependencies:
+ '@electric-sql/pglite':
+ specifier: ^0.5.3
+ version: 0.5.3
+ drizzle-orm:
+ specifier: 'catalog:'
+ version: 1.0.0-beta.22(@electric-sql/pglite@0.5.3)(@types/pg@8.20.0)(pg@8.21.0)
+ pg:
+ specifier: ^8.21.0
+ version: 8.21.0
devDependencies:
+ '@types/node':
+ specifier: 'catalog:'
+ version: 26.0.0
+ '@types/pg':
+ specifier: ^8.20.0
+ version: 8.20.0
+ drizzle-kit:
+ specifier: 1.0.0-beta.22
+ version: 1.0.0-beta.22
tsdown:
specifier: 'catalog:'
- version: 0.22.3
+ version: 0.22.3(typescript@6.0.3)
+ typescript:
+ specifier: 'catalog:'
+ version: 6.0.3
packages:
@@ -41,6 +149,12 @@ packages:
resolution: {integrity: sha512-K8ponJDxBwDHigkeFqaqT5wLGl4bTlwMafR8k7b5CPxr6Ww+UG9ls8Yx6Tcpboxu97eeGVEEyKcHmEyOwN1vSw==}
engines: {node: ^22.18.0 || >=24.11.0}
+ '@drizzle-team/brocli@0.11.0':
+ resolution: {integrity: sha512-hD3pekGiPg0WPCCGAZmusBBJsDqGUR66Y452YgQsZOnkdQ7ViEPKuyP4huUGEZQefp8g34RRodXYmJ2TbCH+tg==}
+
+ '@electric-sql/pglite@0.5.3':
+ resolution: {integrity: sha512-iTTYbA5Uesrl+N7zss0J5LopT7KE4j9aymYo+EZZh+rZbARQCUQOs+n2pay64JRUpc3fCkpfrniTNJnvYzOE+g==}
+
'@emnapi/core@1.11.0':
resolution: {integrity: sha512-l9Oo58x0HOP5znGzVhYW9U3e5wVuA4LAZU2AGezTmkhO1CgQRFDhDg4nneHsu/t3WniXg9QrG2nIXL/ZS8ln8Q==}
@@ -50,6 +164,224 @@ packages:
'@emnapi/wasi-threads@1.2.2':
resolution: {integrity: sha512-c95qOXkHdydNKhscBTebqEC1CVAZpyqOfVfBzQ1qgzyl3gfeldUjIggDbIZgDKsHLgnsM+igH7TJ/eAasaVuMA==}
+ '@envelop/core@5.5.1':
+ resolution: {integrity: sha512-3DQg8sFskDo386TkL5j12jyRAdip/8yzK3x7YGbZBgobZ4aKXrvDU0GppU0SnmrpQnNaiTUsxBs9LKkwQ/eyvw==}
+ engines: {node: '>=18.0.0'}
+
+ '@envelop/instrumentation@1.0.0':
+ resolution: {integrity: sha512-cxgkB66RQB95H3X27jlnxCRNTmPuSTgmBAq6/4n2Dtv4hsk4yz8FadA1ggmd0uZzvKqWD6CR+WFgTjhDqg7eyw==}
+ engines: {node: '>=18.0.0'}
+
+ '@envelop/types@5.2.1':
+ resolution: {integrity: sha512-CsFmA3u3c2QoLDTfEpGr4t25fjMU31nyvse7IzWTvb0ZycuPjMjb0fjlheh+PbhBYb9YLugnT2uY6Mwcg1o+Zg==}
+ engines: {node: '>=18.0.0'}
+
+ '@esbuild/aix-ppc64@0.25.12':
+ resolution: {integrity: sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==}
+ engines: {node: '>=18'}
+ cpu: [ppc64]
+ os: [aix]
+
+ '@esbuild/android-arm64@0.25.12':
+ resolution: {integrity: sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==}
+ engines: {node: '>=18'}
+ cpu: [arm64]
+ os: [android]
+
+ '@esbuild/android-arm@0.25.12':
+ resolution: {integrity: sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==}
+ engines: {node: '>=18'}
+ cpu: [arm]
+ os: [android]
+
+ '@esbuild/android-x64@0.25.12':
+ resolution: {integrity: sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==}
+ engines: {node: '>=18'}
+ cpu: [x64]
+ os: [android]
+
+ '@esbuild/darwin-arm64@0.25.12':
+ resolution: {integrity: sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==}
+ engines: {node: '>=18'}
+ cpu: [arm64]
+ os: [darwin]
+
+ '@esbuild/darwin-x64@0.25.12':
+ resolution: {integrity: sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==}
+ engines: {node: '>=18'}
+ cpu: [x64]
+ os: [darwin]
+
+ '@esbuild/freebsd-arm64@0.25.12':
+ resolution: {integrity: sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==}
+ engines: {node: '>=18'}
+ cpu: [arm64]
+ os: [freebsd]
+
+ '@esbuild/freebsd-x64@0.25.12':
+ resolution: {integrity: sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==}
+ engines: {node: '>=18'}
+ cpu: [x64]
+ os: [freebsd]
+
+ '@esbuild/linux-arm64@0.25.12':
+ resolution: {integrity: sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==}
+ engines: {node: '>=18'}
+ cpu: [arm64]
+ os: [linux]
+
+ '@esbuild/linux-arm@0.25.12':
+ resolution: {integrity: sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==}
+ engines: {node: '>=18'}
+ cpu: [arm]
+ os: [linux]
+
+ '@esbuild/linux-ia32@0.25.12':
+ resolution: {integrity: sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==}
+ engines: {node: '>=18'}
+ cpu: [ia32]
+ os: [linux]
+
+ '@esbuild/linux-loong64@0.25.12':
+ resolution: {integrity: sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==}
+ engines: {node: '>=18'}
+ cpu: [loong64]
+ os: [linux]
+
+ '@esbuild/linux-mips64el@0.25.12':
+ resolution: {integrity: sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==}
+ engines: {node: '>=18'}
+ cpu: [mips64el]
+ os: [linux]
+
+ '@esbuild/linux-ppc64@0.25.12':
+ resolution: {integrity: sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==}
+ engines: {node: '>=18'}
+ cpu: [ppc64]
+ os: [linux]
+
+ '@esbuild/linux-riscv64@0.25.12':
+ resolution: {integrity: sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==}
+ engines: {node: '>=18'}
+ cpu: [riscv64]
+ os: [linux]
+
+ '@esbuild/linux-s390x@0.25.12':
+ resolution: {integrity: sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==}
+ engines: {node: '>=18'}
+ cpu: [s390x]
+ os: [linux]
+
+ '@esbuild/linux-x64@0.25.12':
+ resolution: {integrity: sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==}
+ engines: {node: '>=18'}
+ cpu: [x64]
+ os: [linux]
+
+ '@esbuild/netbsd-arm64@0.25.12':
+ resolution: {integrity: sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==}
+ engines: {node: '>=18'}
+ cpu: [arm64]
+ os: [netbsd]
+
+ '@esbuild/netbsd-x64@0.25.12':
+ resolution: {integrity: sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==}
+ engines: {node: '>=18'}
+ cpu: [x64]
+ os: [netbsd]
+
+ '@esbuild/openbsd-arm64@0.25.12':
+ resolution: {integrity: sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==}
+ engines: {node: '>=18'}
+ cpu: [arm64]
+ os: [openbsd]
+
+ '@esbuild/openbsd-x64@0.25.12':
+ resolution: {integrity: sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==}
+ engines: {node: '>=18'}
+ cpu: [x64]
+ os: [openbsd]
+
+ '@esbuild/openharmony-arm64@0.25.12':
+ resolution: {integrity: sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==}
+ engines: {node: '>=18'}
+ cpu: [arm64]
+ os: [openharmony]
+
+ '@esbuild/sunos-x64@0.25.12':
+ resolution: {integrity: sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==}
+ engines: {node: '>=18'}
+ cpu: [x64]
+ os: [sunos]
+
+ '@esbuild/win32-arm64@0.25.12':
+ resolution: {integrity: sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==}
+ engines: {node: '>=18'}
+ cpu: [arm64]
+ os: [win32]
+
+ '@esbuild/win32-ia32@0.25.12':
+ resolution: {integrity: sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==}
+ engines: {node: '>=18'}
+ cpu: [ia32]
+ os: [win32]
+
+ '@esbuild/win32-x64@0.25.12':
+ resolution: {integrity: sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==}
+ engines: {node: '>=18'}
+ cpu: [x64]
+ os: [win32]
+
+ '@fastify/busboy@3.2.0':
+ resolution: {integrity: sha512-m9FVDXU3GT2ITSe0UaMA5rU3QkfC/UXtCU8y0gSN/GugTqtVldOBWIB5V6V3sbmenVZUIpU6f+mPEO2+m5iTaA==}
+
+ '@graphql-tools/executor@1.5.3':
+ resolution: {integrity: sha512-mgBFC0bsrZPZLu9EnydpMnAuQ8Iiq0CEbUcsmvXsm2/iYektGHDN/+bmb7hicA6dWZtdPfklYJmr21WD0GnOfA==}
+ engines: {node: '>=16.0.0'}
+ peerDependencies:
+ graphql: ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0
+
+ '@graphql-tools/merge@9.1.9':
+ resolution: {integrity: sha512-iHUWNjRHeQRYdgIMIuChThOwoKzA9vrzYeslgfBo5eUYEyHGZCoDPjAavssoYXLwstYt1dZj2J22jSzc2DrN0Q==}
+ engines: {node: '>=16.0.0'}
+ peerDependencies:
+ graphql: ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0
+
+ '@graphql-tools/schema@10.0.33':
+ resolution: {integrity: sha512-O6P3RIftO0jafnSsFAqpjurUuUxJ43s/AdPVLQsBkI6y4Ic/tKm4C1Qm1KKQsCDTOxXPJClh/v3g7k7yLKCFBQ==}
+ engines: {node: '>=16.0.0'}
+ peerDependencies:
+ graphql: ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0
+
+ '@graphql-tools/utils@10.11.0':
+ resolution: {integrity: sha512-iBFR9GXIs0gCD+yc3hoNswViL1O5josI33dUqiNStFI/MHLCEPduasceAcazRH77YONKNiviHBV8f7OgcT4o2Q==}
+ engines: {node: '>=16.0.0'}
+ peerDependencies:
+ graphql: ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0
+
+ '@graphql-tools/utils@11.1.0':
+ resolution: {integrity: sha512-PtFVG4r8Z2LEBSaPYQMusBiB3o6kjLVJyjCLbnWem/SpSuM21v6LTmgpkXfYU1qpBV2UGsFyuEnSJInl8fR1Ag==}
+ engines: {node: '>=16.0.0'}
+ peerDependencies:
+ graphql: ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0
+
+ '@graphql-typed-document-node/core@3.2.0':
+ resolution: {integrity: sha512-mB9oAsNCm9aM3/SOv4YtBMqZbYj10R7dkq8byBqxGY/ncFwhf2oQzMV+LCRlWoDSEBJ3COiR1yeDvMtsoOsuFQ==}
+ peerDependencies:
+ graphql: ^0.8.0 || ^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0
+
+ '@graphql-yoga/logger@2.0.1':
+ resolution: {integrity: sha512-Nv0BoDGLMg9QBKy9cIswQ3/6aKaKjlTh87x3GiBg2Z4RrjyrM48DvOOK0pJh1C1At+b0mUIM67cwZcFTDLN4sA==}
+ engines: {node: '>=18.0.0'}
+
+ '@graphql-yoga/subscription@5.0.5':
+ resolution: {integrity: sha512-oCMWOqFs6QV96/NZRt/ZhTQvzjkGB4YohBOpKM4jH/lDT4qb7Lex/aGCxpi/JD9njw3zBBtMqxbaC22+tFHVvw==}
+ engines: {node: '>=18.0.0'}
+
+ '@graphql-yoga/typed-event-target@3.0.2':
+ resolution: {integrity: sha512-ZpJxMqB+Qfe3rp6uszCQoag4nSw42icURnBRfFYSOmTgEeOe4rD0vYlbA8spvCu2TlCesNTlEN9BLWtQqLxabA==}
+ engines: {node: '>=18.0.0'}
+
'@jridgewell/gen-mapping@0.3.13':
resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==}
@@ -63,18 +395,51 @@ packages:
'@jridgewell/trace-mapping@0.3.31':
resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==}
+ '@js-temporal/polyfill@0.5.1':
+ resolution: {integrity: sha512-hloP58zRVCRSpgDxmqCWJNlizAlUgJFqG2ypq79DCvyv9tHjRYMDOcPFjzfl/A1/YxDvRCZz8wvZvmapQnKwFQ==}
+ engines: {node: '>=12'}
+
'@napi-rs/wasm-runtime@1.1.5':
resolution: {integrity: sha512-AWPoBRJ9tsnVhor4sjO7rkni+7p+2IAEFj6cx06UgP10jkQHqay/36uRV/bFkgrh18D9vb4cr8Q0Pthskgzy+Q==}
peerDependencies:
'@emnapi/core': ^1.7.1
'@emnapi/runtime': ^1.7.1
+ '@optique/core@1.1.0':
+ resolution: {integrity: sha512-eBqai76tHiFDoShlTNXN9AAPs9XznCJRrk4qmGhjZUSMmePCF9o1XU3okUHxHdDXXCHj4auKoIvCN79KNKErxA==}
+ engines: {bun: '>=1.2.0', deno: '>=2.3.0', node: '>=20.0.0'}
+
+ '@optique/run@1.1.0':
+ resolution: {integrity: sha512-dcuqqqU1Cpm9CLGEkCkpT/cpJ6H6a+hs0rP+iD8Tgwb+CPPZtX/hCfdIrqYyZ2RtYLxgc3S6KqC81AZAwEUPew==}
+ engines: {bun: '>=1.2.0', deno: '>=2.3.0', node: '>=20.0.0'}
+
'@oxc-project/types@0.135.0':
resolution: {integrity: sha512-wR+xRdFkUBMvcAjBJ2q2kcZM6d+DKu2NgoOyxZgYwZdLhmiv6+rnO8PZ/P68kMiZtIKm+pW7zyEJ4kSOs0vo+Q==}
+ '@pothos/core@4.13.0':
+ resolution: {integrity: sha512-bIaVdLTkwPkkmIn0Ji13vsc9Zy0mEi++a9iMFIRVa88l0G7JuFJrJX5qGPo1GGu4drvRW4SAr2aDm2V5KcYj4A==}
+ peerDependencies:
+ graphql: ^16.10.0 || ^17.0.0
+
+ '@pothos/plugin-drizzle@0.17.4':
+ resolution: {integrity: sha512-cAaMexSSOtoXf7Khj62yOHGxHyaOnuIzNdgl+w0rNfnJdtVOr4slQjHinWJWzYWdRauOclZddxWbLaou4NK0QQ==}
+ peerDependencies:
+ '@pothos/core': '*'
+ drizzle-orm: '>=1.0.0-beta.2'
+ graphql: ^16.10.0
+
+ '@pothos/plugin-relay@4.7.0':
+ resolution: {integrity: sha512-IQ7f7WLu7uXxiZBcJgUTdspjIVzX3bgvd8XjnzxGJUbn/uE5HlVyCFmvhJffHIIS2OKSHhOOJRMwIwVwauwnSg==}
+ peerDependencies:
+ '@pothos/core': '*'
+ graphql: ^16.10.0
+
'@quansync/fs@1.0.0':
resolution: {integrity: sha512-4TJ3DFtlf1L5LDMaM6CanJ/0lckGNtJcMjQ1NAV6zDmA0tEHKZtxNKin8EgPaVX1YzljbxckyT2tJrpQKAtngQ==}
+ '@repeaterjs/repeater@3.1.0':
+ resolution: {integrity: sha512-TaoVksZRSx2KWYYpyLQtMQXXeS98VsgZImzW65xmiVgbYhXLk+aEsmzPLirqVuE4/XuUapH2iMtxUzaBNDzdSQ==}
+
'@rolldown/binding-android-arm64@1.1.1':
resolution: {integrity: sha512-BLf9Wak/gfwVb7NQTQW4wBgL3oAfPy7ArEkhwV543OVw/uY6B47z5xYsqPSZ9PDOorvURPinws6ThaFuNgGLgA==}
engines: {node: ^20.19.0 || >=22.12.0}
@@ -182,6 +547,36 @@ packages:
'@types/jsesc@2.5.1':
resolution: {integrity: sha512-9VN+6yxLOPLOav+7PwjZbxiID2bVaeq0ED4qSQmdQTdjnXJSaCVKTR58t15oqH1H5t8Ng2ZX1SabJVoN9Q34bw==}
+ '@types/node@26.0.0':
+ resolution: {integrity: sha512-vf2YFi1iY9lHGwNJMs01biZFbKJkrZR1T6/MlzjhJLPdntOHLhTrDSnSVcdtvjihi4VQNlrFRIxLsDBlQpAipA==}
+
+ '@types/pg@8.20.0':
+ resolution: {integrity: sha512-bEPFOaMAHTEP1EzpvHTbmwR8UsFyHSKsRisLIHVMXnpNefSbGA1bD6CVy+qKjGSqmZqNqBDV2azOBo8TgkcVow==}
+
+ '@whatwg-node/disposablestack@0.0.6':
+ resolution: {integrity: sha512-LOtTn+JgJvX8WfBVJtF08TGrdjuFzGJc4mkP8EdDI8ADbvO7kiexYep1o8dwnt0okb0jYclCDXF13xU7Ge4zSw==}
+ engines: {node: '>=18.0.0'}
+
+ '@whatwg-node/events@0.1.2':
+ resolution: {integrity: sha512-ApcWxkrs1WmEMS2CaLLFUEem/49erT3sxIVjpzU5f6zmVcnijtDSrhoK2zVobOIikZJdH63jdAXOrvjf6eOUNQ==}
+ engines: {node: '>=18.0.0'}
+
+ '@whatwg-node/fetch@0.10.13':
+ resolution: {integrity: sha512-b4PhJ+zYj4357zwk4TTuF2nEe0vVtOrwdsrNo5hL+u1ojXNhh1FgJ6pg1jzDlwlT4oBdzfSwaBwMCtFCsIWg8Q==}
+ engines: {node: '>=18.0.0'}
+
+ '@whatwg-node/node-fetch@0.8.6':
+ resolution: {integrity: sha512-BDMdYFcerLQkwA2RTldxOqRCs6ZQD1S7UgP3pUdGUkcbgTrP/V5ko77ZkCww9DHmC4lpoYuwigGfQYj285gMvA==}
+ engines: {node: '>=18.0.0'}
+
+ '@whatwg-node/promise-helpers@1.3.2':
+ resolution: {integrity: sha512-Nst5JdK47VIl9UcGwtv2Rcgyn5lWtZ0/mhRQ4G8NN2isxpq2TO30iqHzmwoJycjWuyUfg3GFXqP/gFHXeV57IA==}
+ engines: {node: '>=16.0.0'}
+
+ '@whatwg-node/server@0.11.0':
+ resolution: {integrity: sha512-VSdkwnJRr8Yv9UgB2aXB3VUPWwd6Oqnn0hycFwhg9pZgWxJXb7JmhsiXe9tmpMwjHFxli12PGcz9aI63YYloGQ==}
+ engines: {node: '>=18.0.0'}
+
ansis@4.3.1:
resolution: {integrity: sha512-BJ8/l4R5LRE7hW9WdSuGYrLSHi2ynxeFpDFbH0K/CgNeY/tyhk+vO6TYxXC5r5CpUhNVX310xzPsN/H9lCdfOA==}
engines: {node: '>=14'}
@@ -197,9 +592,136 @@ packages:
resolution: {integrity: sha512-tixWYgm5ZoOD+3g6UTea91eow5z6AAHaho3g0V9CNSNb45gM8SmflpAc+GRd1InC4AqN/07Unrgp56Y94N9hJQ==}
engines: {node: '>=20.19.0'}
+ cross-inspect@1.0.1:
+ resolution: {integrity: sha512-Pcw1JTvZLSJH83iiGWt6fRcT+BjZlCDRVwYLbUcHzv/CRpB7r0MlSrGbIyQvVSNyGnbt7G4AXuyCiDR3POvZ1A==}
+ engines: {node: '>=16.0.0'}
+
defu@6.1.7:
resolution: {integrity: sha512-7z22QmUWiQ/2d0KkdYmANbRUVABpZ9SNYyH5vx6PZ+nE5bcC0l7uFvEfHlyld/HcGBFTL536ClDt3DEcSlEJAQ==}
+ drizzle-kit@1.0.0-beta.22:
+ resolution: {integrity: sha512-9HTZuQRljQKTgCx4UhiGn8KYYfHGk4+B/bRR1714W67kz0qgJvdrG527i8rQD8uUyET9UTGR1u8syySJD4znGw==}
+ hasBin: true
+
+ drizzle-orm@1.0.0-beta.22:
+ resolution: {integrity: sha512-F+DZyVIvH0oVKa/w08Cle1xfoH+pc+htIXHG/frnMLG72aby9NYYr9oc+9XvghnoO4umxFItduz0OMmQJMnenw==}
+ peerDependencies:
+ '@aws-sdk/client-rds-data': '>=3'
+ '@cloudflare/workers-types': '>=4'
+ '@effect/sql': ^0.48.5
+ '@effect/sql-pg': ^0.49.7
+ '@electric-sql/pglite': '>=0.2.0'
+ '@libsql/client': '>=0.10.0'
+ '@libsql/client-wasm': '>=0.10.0'
+ '@neondatabase/serverless': '>=0.10.0'
+ '@op-engineering/op-sqlite': '>=2'
+ '@opentelemetry/api': ^1.4.1
+ '@planetscale/database': '>=1.13'
+ '@sinclair/typebox': '>=0.34.8'
+ '@sqlitecloud/drivers': '>=1.0.653'
+ '@tidbcloud/serverless': '*'
+ '@tursodatabase/database': '>=0.2.1'
+ '@tursodatabase/database-common': '>=0.2.1'
+ '@tursodatabase/database-wasm': '>=0.2.1'
+ '@types/better-sqlite3': '*'
+ '@types/mssql': ^9.1.4
+ '@types/pg': '*'
+ '@types/sql.js': '*'
+ '@upstash/redis': '>=1.34.7'
+ '@vercel/postgres': '>=0.8.0'
+ '@xata.io/client': '*'
+ arktype: '>=2.0.0'
+ better-sqlite3: '>=9.3.0'
+ bun-types: '*'
+ expo-sqlite: '>=14.0.0'
+ gel: '>=2'
+ mssql: ^11.0.1
+ mysql2: '>=2'
+ pg: '>=8'
+ postgres: '>=3'
+ sql.js: '>=1'
+ sqlite3: '>=5'
+ typebox: '>=1.0.0'
+ valibot: '>=1.0.0-beta.7'
+ zod: ^3.25.0 || ^4.0.0
+ peerDependenciesMeta:
+ '@aws-sdk/client-rds-data':
+ optional: true
+ '@cloudflare/workers-types':
+ optional: true
+ '@effect/sql':
+ optional: true
+ '@effect/sql-pg':
+ optional: true
+ '@electric-sql/pglite':
+ optional: true
+ '@libsql/client':
+ optional: true
+ '@libsql/client-wasm':
+ optional: true
+ '@neondatabase/serverless':
+ optional: true
+ '@op-engineering/op-sqlite':
+ optional: true
+ '@opentelemetry/api':
+ optional: true
+ '@planetscale/database':
+ optional: true
+ '@sinclair/typebox':
+ optional: true
+ '@sqlitecloud/drivers':
+ optional: true
+ '@tidbcloud/serverless':
+ optional: true
+ '@tursodatabase/database':
+ optional: true
+ '@tursodatabase/database-common':
+ optional: true
+ '@tursodatabase/database-wasm':
+ optional: true
+ '@types/better-sqlite3':
+ optional: true
+ '@types/mssql':
+ optional: true
+ '@types/pg':
+ optional: true
+ '@types/sql.js':
+ optional: true
+ '@upstash/redis':
+ optional: true
+ '@vercel/postgres':
+ optional: true
+ '@xata.io/client':
+ optional: true
+ arktype:
+ optional: true
+ better-sqlite3:
+ optional: true
+ bun-types:
+ optional: true
+ expo-sqlite:
+ optional: true
+ gel:
+ optional: true
+ mssql:
+ optional: true
+ mysql2:
+ optional: true
+ pg:
+ optional: true
+ postgres:
+ optional: true
+ sql.js:
+ optional: true
+ sqlite3:
+ optional: true
+ typebox:
+ optional: true
+ valibot:
+ optional: true
+ zod:
+ optional: true
+
dts-resolver@3.0.0:
resolution: {integrity: sha512-1T1f+z+4tl9XD+m+0HBgWoL/nm0bOIffyWaUuUSBlFg/86IWvfx+wjNaO/ybU0AJzG9/Mi5hBUgGV6zCmWEN7Q==}
engines: {node: ^22.18.0 || >=24.0.0}
@@ -213,6 +735,11 @@ packages:
resolution: {integrity: sha512-YGRs8knHhKHVShLkFET/rWAU8kmHbOV5LwN938RHI0pljAJ1Gf6SzXsSmRaEzcXTtOOmVqJ5+WtQPL5uigY50Q==}
engines: {node: '>=14'}
+ esbuild@0.25.12:
+ resolution: {integrity: sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==}
+ engines: {node: '>=18'}
+ hasBin: true
+
estree-walker@3.0.3:
resolution: {integrity: sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==}
@@ -225,10 +752,29 @@ packages:
picomatch:
optional: true
+ get-tsconfig@4.14.0:
+ resolution: {integrity: sha512-yTb+8DXzDREzgvYmh6s9vHsSVCHeC0G3PI5bEXNBHtmshPnO+S5O7qgLEOn0I5QvMy6kpZN8K1NKGyilLb93wA==}
+
get-tsconfig@5.0.0-beta.5:
resolution: {integrity: sha512-/6gFNr0N04nob252sTQxyFLi3eKFRqIg1I87YcqAMT1i6SQrSF6KujUEQrtrjMV0H/eejTCltLdDSTEMzHbnsQ==}
engines: {node: '>=20.20.0'}
+ graphql-scalars@1.25.0:
+ resolution: {integrity: sha512-b0xyXZeRFkne4Eq7NAnL400gStGqG/Sx9VqX0A05nHyEbv57UJnWKsjNnrpVqv5e/8N1MUxkt0wwcRXbiyKcFg==}
+ engines: {node: '>=10'}
+ peerDependencies:
+ graphql: ^0.8.0 || ^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0
+
+ graphql-yoga@5.21.2:
+ resolution: {integrity: sha512-IIRF/3xtjj2D6caAWL9177hQ8tV3mWB3hve1GRnz7njPhQ3iY1jFtSp98fNGv0yV9kaPh9kKQ8JWdJZnedVmDw==}
+ engines: {node: '>=18.0.0'}
+ peerDependencies:
+ graphql: ^15.2.0 || ^16.0.0
+
+ graphql@16.14.2:
+ resolution: {integrity: sha512-Chq1s4CY7jmh8gO2qvLIJyfCDIN+EHLFW/9iShnp1z8FjBQMoodWP1kDC36VAMXXIvAjj4ARa7ntfAV2BrjsbA==}
+ engines: {node: ^12.22.0 || ^14.16.0 || ^16.0.0 || >=17.0.0}
+
hookable@6.1.1:
resolution: {integrity: sha512-U9LYDy1CwhMCnprUfeAZWZGByVbhd54hwepegYTK7Pi5NvqEj63ifz5z+xukznehT7i6NIZRu89Ay1AZmRsLEQ==}
@@ -236,11 +782,21 @@ packages:
resolution: {integrity: sha512-NkJQA7oZ4YHQhd2+H3BoRFKF3d/XNsiKpHZCQEMH9pDX27hQQLsTyOocyRgaIVtf8gHX3Nt3LPkR4e5EdtPAGQ==}
engines: {node: ^22.18.0 || >=24.0.0}
+ jiti@2.7.0:
+ resolution: {integrity: sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ==}
+ hasBin: true
+
+ jsbi@4.3.2:
+ resolution: {integrity: sha512-9fqMSQbhJykSeii05nxKl4m6Eqn2P6rOlYiS+C5Dr/HPIU/7yZxu5qzbs40tgaFORiw2Amd0mirjxatXYMkIew==}
+
jsesc@3.1.0:
resolution: {integrity: sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==}
engines: {node: '>=6'}
hasBin: true
+ lru-cache@10.4.3:
+ resolution: {integrity: sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==}
+
obug@2.1.3:
resolution: {integrity: sha512-9miFgM2OFba7hB+pRgvtV84pYTBaoTHohvmIgiRt6dRIzbwEOIaNaP+dIlGs2fNFoB0SeISs0Jz5WFVRid6Xyg==}
engines: {node: '>=12.20.0'}
@@ -248,10 +804,60 @@ packages:
pathe@2.0.3:
resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==}
+ pg-cloudflare@1.4.0:
+ resolution: {integrity: sha512-Vo7z/6rrQYxpNRylp4Tlob2elzbh+N/MOQbxFVWCxS7oEx6jF53GTJFxK2WWpKuBRkmiin4Mt+xofFDjx09R0A==}
+
+ pg-connection-string@2.13.0:
+ resolution: {integrity: sha512-EMnU9E2fSULdsbErBbMaXJvFeD9B4+nPcM3f+4lsiCR0BHLPrLVjv3DbyM2hgQQviKJaTWIRRTjKjWlHg3p2ig==}
+
+ pg-int8@1.0.1:
+ resolution: {integrity: sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw==}
+ engines: {node: '>=4.0.0'}
+
+ pg-pool@3.14.0:
+ resolution: {integrity: sha512-gKtPkFdQPU3DksooVLi9LsjZxrsBUZIpa+7aVx+LV5pNh0KzP4Zleud2po+ConrxbuXGBJ6Hfer6hdgpIBpBaw==}
+ peerDependencies:
+ pg: '>=8.0'
+
+ pg-protocol@1.14.0:
+ resolution: {integrity: sha512-n5taZ1kO3s9ngDTVxsEznOqCyToTgz0FLuPq0B33COy5pPpuWJpY3/2oRBVETuOgzdqRXfWpM9HIhp2LBBT1BA==}
+
+ pg-types@2.2.0:
+ resolution: {integrity: sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA==}
+ engines: {node: '>=4'}
+
+ pg@8.21.0:
+ resolution: {integrity: sha512-AUP1EYJuHraQGsVoCQVIcM7TEJVGtDzxWtGFZd8rds9d+CCXlU5Js1rYgfLNvxy9iJrpHjGrRjoi/3BT9fRyiA==}
+ engines: {node: '>= 16.0.0'}
+ peerDependencies:
+ pg-native: '>=3.0.1'
+ peerDependenciesMeta:
+ pg-native:
+ optional: true
+
+ pgpass@1.0.5:
+ resolution: {integrity: sha512-FdW9r/jQZhSeohs1Z3sI1yxFQNFvMcnmfuj4WBMUTxOrAyLMaTcE1aAMBiTlbMNaXvBCQuVi0R7hd8udDSP7ug==}
+
picomatch@4.0.4:
resolution: {integrity: sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==}
engines: {node: '>=12'}
+ postgres-array@2.0.0:
+ resolution: {integrity: sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA==}
+ engines: {node: '>=4'}
+
+ postgres-bytea@1.0.1:
+ resolution: {integrity: sha512-5+5HqXnsZPE65IJZSMkZtURARZelel2oXUEO8rH83VS/hxH5vv1uHquPg5wZs8yMAfdv971IU+kcPUczi7NVBQ==}
+ engines: {node: '>=0.10.0'}
+
+ postgres-date@1.0.7:
+ resolution: {integrity: sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q==}
+ engines: {node: '>=0.10.0'}
+
+ postgres-interval@1.2.0:
+ resolution: {integrity: sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ==}
+ engines: {node: '>=0.10.0'}
+
quansync@1.0.0:
resolution: {integrity: sha512-5xZacEEufv3HSTPQuchrvV6soaiACMFnq1H8wkVioctoH3TRha9Sz66lOxRwPK/qZj7HPiSveih9yAyh98gvqA==}
@@ -287,6 +893,15 @@ packages:
engines: {node: '>=10'}
hasBin: true
+ split2@4.2.0:
+ resolution: {integrity: sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==}
+ engines: {node: '>= 10.x'}
+
+ srvx@0.11.16:
+ resolution: {integrity: sha512-bp07zRuycfTY43IjAvvTFnmnJi8ikW0VFiHwOhhYcVW/L4xQ1XY4PAd4Nuum1rsA17C39zL7x+CDhrn5AL32Rw==}
+ engines: {node: '>=20.16.0'}
+ hasBin: true
+
tinyexec@1.2.4:
resolution: {integrity: sha512-SHf/r48b7vOrjve9PxJo3MN5v5yuyjHvdUcrQffT3WXMUfnGmHDVbC4k3sHJaJTgZCwpUplIaAo5ANtMyp3YHg==}
engines: {node: '>=18'}
@@ -336,9 +951,24 @@ packages:
tslib@2.8.1:
resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==}
+ typescript@6.0.3:
+ resolution: {integrity: sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==}
+ engines: {node: '>=14.17'}
+ hasBin: true
+
unconfig-core@7.5.0:
resolution: {integrity: sha512-Su3FauozOGP44ZmKdHy2oE6LPjk51M/TRRjHv2HNCWiDvfvCoxC2lno6jevMA91MYAdCdwP05QnWdWpSbncX/w==}
+ undici-types@8.3.0:
+ resolution: {integrity: sha512-j375ScV60dom+YkPFIfTLcOiPxkN/buHz5GobjLhixFuANaNs3C9l4GmrWqejgXWJ7BbJcFYpTEUkS1Ge8bpZQ==}
+
+ urlpattern-polyfill@10.1.0:
+ resolution: {integrity: sha512-IGjKp/o0NL3Bso1PymYURCJxMPNAf/ILOpendP9f5B6e1rTJgdgiOvgfoT8VxCAdY+Wisb9uhGaJJf3yZ2V9nw==}
+
+ xtend@4.0.2:
+ resolution: {integrity: sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==}
+ engines: {node: '>=0.4'}
+
snapshots:
'@babel/generator@8.0.0':
@@ -363,6 +993,10 @@ snapshots:
'@babel/helper-string-parser': 8.0.0
'@babel/helper-validator-identifier': 8.0.0
+ '@drizzle-team/brocli@0.11.0': {}
+
+ '@electric-sql/pglite@0.5.3': {}
+
'@emnapi/core@1.11.0':
dependencies:
'@emnapi/wasi-threads': 1.2.2
@@ -379,6 +1013,162 @@ snapshots:
tslib: 2.8.1
optional: true
+ '@envelop/core@5.5.1':
+ dependencies:
+ '@envelop/instrumentation': 1.0.0
+ '@envelop/types': 5.2.1
+ '@whatwg-node/promise-helpers': 1.3.2
+ tslib: 2.8.1
+
+ '@envelop/instrumentation@1.0.0':
+ dependencies:
+ '@whatwg-node/promise-helpers': 1.3.2
+ tslib: 2.8.1
+
+ '@envelop/types@5.2.1':
+ dependencies:
+ '@whatwg-node/promise-helpers': 1.3.2
+ tslib: 2.8.1
+
+ '@esbuild/aix-ppc64@0.25.12':
+ optional: true
+
+ '@esbuild/android-arm64@0.25.12':
+ optional: true
+
+ '@esbuild/android-arm@0.25.12':
+ optional: true
+
+ '@esbuild/android-x64@0.25.12':
+ optional: true
+
+ '@esbuild/darwin-arm64@0.25.12':
+ optional: true
+
+ '@esbuild/darwin-x64@0.25.12':
+ optional: true
+
+ '@esbuild/freebsd-arm64@0.25.12':
+ optional: true
+
+ '@esbuild/freebsd-x64@0.25.12':
+ optional: true
+
+ '@esbuild/linux-arm64@0.25.12':
+ optional: true
+
+ '@esbuild/linux-arm@0.25.12':
+ optional: true
+
+ '@esbuild/linux-ia32@0.25.12':
+ optional: true
+
+ '@esbuild/linux-loong64@0.25.12':
+ optional: true
+
+ '@esbuild/linux-mips64el@0.25.12':
+ optional: true
+
+ '@esbuild/linux-ppc64@0.25.12':
+ optional: true
+
+ '@esbuild/linux-riscv64@0.25.12':
+ optional: true
+
+ '@esbuild/linux-s390x@0.25.12':
+ optional: true
+
+ '@esbuild/linux-x64@0.25.12':
+ optional: true
+
+ '@esbuild/netbsd-arm64@0.25.12':
+ optional: true
+
+ '@esbuild/netbsd-x64@0.25.12':
+ optional: true
+
+ '@esbuild/openbsd-arm64@0.25.12':
+ optional: true
+
+ '@esbuild/openbsd-x64@0.25.12':
+ optional: true
+
+ '@esbuild/openharmony-arm64@0.25.12':
+ optional: true
+
+ '@esbuild/sunos-x64@0.25.12':
+ optional: true
+
+ '@esbuild/win32-arm64@0.25.12':
+ optional: true
+
+ '@esbuild/win32-ia32@0.25.12':
+ optional: true
+
+ '@esbuild/win32-x64@0.25.12':
+ optional: true
+
+ '@fastify/busboy@3.2.0': {}
+
+ '@graphql-tools/executor@1.5.3(graphql@16.14.2)':
+ dependencies:
+ '@graphql-tools/utils': 11.1.0(graphql@16.14.2)
+ '@graphql-typed-document-node/core': 3.2.0(graphql@16.14.2)
+ '@repeaterjs/repeater': 3.1.0
+ '@whatwg-node/disposablestack': 0.0.6
+ '@whatwg-node/promise-helpers': 1.3.2
+ graphql: 16.14.2
+ tslib: 2.8.1
+
+ '@graphql-tools/merge@9.1.9(graphql@16.14.2)':
+ dependencies:
+ '@graphql-tools/utils': 11.1.0(graphql@16.14.2)
+ graphql: 16.14.2
+ tslib: 2.8.1
+
+ '@graphql-tools/schema@10.0.33(graphql@16.14.2)':
+ dependencies:
+ '@graphql-tools/merge': 9.1.9(graphql@16.14.2)
+ '@graphql-tools/utils': 11.1.0(graphql@16.14.2)
+ graphql: 16.14.2
+ tslib: 2.8.1
+
+ '@graphql-tools/utils@10.11.0(graphql@16.14.2)':
+ dependencies:
+ '@graphql-typed-document-node/core': 3.2.0(graphql@16.14.2)
+ '@whatwg-node/promise-helpers': 1.3.2
+ cross-inspect: 1.0.1
+ graphql: 16.14.2
+ tslib: 2.8.1
+
+ '@graphql-tools/utils@11.1.0(graphql@16.14.2)':
+ dependencies:
+ '@graphql-typed-document-node/core': 3.2.0(graphql@16.14.2)
+ '@whatwg-node/promise-helpers': 1.3.2
+ cross-inspect: 1.0.1
+ graphql: 16.14.2
+ tslib: 2.8.1
+
+ '@graphql-typed-document-node/core@3.2.0(graphql@16.14.2)':
+ dependencies:
+ graphql: 16.14.2
+
+ '@graphql-yoga/logger@2.0.1':
+ dependencies:
+ tslib: 2.8.1
+
+ '@graphql-yoga/subscription@5.0.5':
+ dependencies:
+ '@graphql-yoga/typed-event-target': 3.0.2
+ '@repeaterjs/repeater': 3.1.0
+ '@whatwg-node/events': 0.1.2
+ tslib: 2.8.1
+
+ '@graphql-yoga/typed-event-target@3.0.2':
+ dependencies:
+ '@repeaterjs/repeater': 3.1.0
+ tslib: 2.8.1
+
'@jridgewell/gen-mapping@0.3.13':
dependencies:
'@jridgewell/sourcemap-codec': 1.5.5
@@ -393,6 +1183,10 @@ snapshots:
'@jridgewell/resolve-uri': 3.1.2
'@jridgewell/sourcemap-codec': 1.5.5
+ '@js-temporal/polyfill@0.5.1':
+ dependencies:
+ jsbi: 4.3.2
+
'@napi-rs/wasm-runtime@1.1.5(@emnapi/core@1.11.0)(@emnapi/runtime@1.11.0)':
dependencies:
'@emnapi/core': 1.11.0
@@ -400,12 +1194,35 @@ snapshots:
'@tybys/wasm-util': 0.10.2
optional: true
+ '@optique/core@1.1.0': {}
+
+ '@optique/run@1.1.0':
+ dependencies:
+ '@optique/core': 1.1.0
+
'@oxc-project/types@0.135.0': {}
+ '@pothos/core@4.13.0(graphql@16.14.2)':
+ dependencies:
+ graphql: 16.14.2
+
+ '@pothos/plugin-drizzle@0.17.4(@pothos/core@4.13.0(graphql@16.14.2))(drizzle-orm@1.0.0-beta.22(@electric-sql/pglite@0.5.3)(@types/pg@8.20.0)(pg@8.21.0))(graphql@16.14.2)':
+ dependencies:
+ '@pothos/core': 4.13.0(graphql@16.14.2)
+ drizzle-orm: 1.0.0-beta.22(@electric-sql/pglite@0.5.3)(@types/pg@8.20.0)(pg@8.21.0)
+ graphql: 16.14.2
+
+ '@pothos/plugin-relay@4.7.0(@pothos/core@4.13.0(graphql@16.14.2))(graphql@16.14.2)':
+ dependencies:
+ '@pothos/core': 4.13.0(graphql@16.14.2)
+ graphql: 16.14.2
+
'@quansync/fs@1.0.0':
dependencies:
quansync: 1.0.0
+ '@repeaterjs/repeater@3.1.0': {}
+
'@rolldown/binding-android-arm64@1.1.1':
optional: true
@@ -466,6 +1283,49 @@ snapshots:
'@types/jsesc@2.5.1': {}
+ '@types/node@26.0.0':
+ dependencies:
+ undici-types: 8.3.0
+
+ '@types/pg@8.20.0':
+ dependencies:
+ '@types/node': 26.0.0
+ pg-protocol: 1.14.0
+ pg-types: 2.2.0
+
+ '@whatwg-node/disposablestack@0.0.6':
+ dependencies:
+ '@whatwg-node/promise-helpers': 1.3.2
+ tslib: 2.8.1
+
+ '@whatwg-node/events@0.1.2':
+ dependencies:
+ tslib: 2.8.1
+
+ '@whatwg-node/fetch@0.10.13':
+ dependencies:
+ '@whatwg-node/node-fetch': 0.8.6
+ urlpattern-polyfill: 10.1.0
+
+ '@whatwg-node/node-fetch@0.8.6':
+ dependencies:
+ '@fastify/busboy': 3.2.0
+ '@whatwg-node/disposablestack': 0.0.6
+ '@whatwg-node/promise-helpers': 1.3.2
+ tslib: 2.8.1
+
+ '@whatwg-node/promise-helpers@1.3.2':
+ dependencies:
+ tslib: 2.8.1
+
+ '@whatwg-node/server@0.11.0':
+ dependencies:
+ '@envelop/instrumentation': 1.0.0
+ '@whatwg-node/disposablestack': 0.0.6
+ '@whatwg-node/fetch': 0.10.13
+ '@whatwg-node/promise-helpers': 1.3.2
+ tslib: 2.8.1
+
ansis@4.3.1: {}
ast-kit@3.0.0:
@@ -478,12 +1338,59 @@ snapshots:
cac@7.0.0: {}
+ cross-inspect@1.0.1:
+ dependencies:
+ tslib: 2.8.1
+
defu@6.1.7: {}
+ drizzle-kit@1.0.0-beta.22:
+ dependencies:
+ '@drizzle-team/brocli': 0.11.0
+ '@js-temporal/polyfill': 0.5.1
+ esbuild: 0.25.12
+ get-tsconfig: 4.14.0
+ jiti: 2.7.0
+
+ drizzle-orm@1.0.0-beta.22(@electric-sql/pglite@0.5.3)(@types/pg@8.20.0)(pg@8.21.0):
+ optionalDependencies:
+ '@electric-sql/pglite': 0.5.3
+ '@types/pg': 8.20.0
+ pg: 8.21.0
+
dts-resolver@3.0.0: {}
empathic@2.0.1: {}
+ esbuild@0.25.12:
+ optionalDependencies:
+ '@esbuild/aix-ppc64': 0.25.12
+ '@esbuild/android-arm': 0.25.12
+ '@esbuild/android-arm64': 0.25.12
+ '@esbuild/android-x64': 0.25.12
+ '@esbuild/darwin-arm64': 0.25.12
+ '@esbuild/darwin-x64': 0.25.12
+ '@esbuild/freebsd-arm64': 0.25.12
+ '@esbuild/freebsd-x64': 0.25.12
+ '@esbuild/linux-arm': 0.25.12
+ '@esbuild/linux-arm64': 0.25.12
+ '@esbuild/linux-ia32': 0.25.12
+ '@esbuild/linux-loong64': 0.25.12
+ '@esbuild/linux-mips64el': 0.25.12
+ '@esbuild/linux-ppc64': 0.25.12
+ '@esbuild/linux-riscv64': 0.25.12
+ '@esbuild/linux-s390x': 0.25.12
+ '@esbuild/linux-x64': 0.25.12
+ '@esbuild/netbsd-arm64': 0.25.12
+ '@esbuild/netbsd-x64': 0.25.12
+ '@esbuild/openbsd-arm64': 0.25.12
+ '@esbuild/openbsd-x64': 0.25.12
+ '@esbuild/openharmony-arm64': 0.25.12
+ '@esbuild/sunos-x64': 0.25.12
+ '@esbuild/win32-arm64': 0.25.12
+ '@esbuild/win32-ia32': 0.25.12
+ '@esbuild/win32-x64': 0.25.12
+
estree-walker@3.0.3:
dependencies:
'@types/estree': 1.0.9
@@ -492,27 +1399,105 @@ snapshots:
optionalDependencies:
picomatch: 4.0.4
+ get-tsconfig@4.14.0:
+ dependencies:
+ resolve-pkg-maps: 1.0.0
+
get-tsconfig@5.0.0-beta.5:
dependencies:
resolve-pkg-maps: 1.0.0
+ graphql-scalars@1.25.0(graphql@16.14.2):
+ dependencies:
+ graphql: 16.14.2
+ tslib: 2.8.1
+
+ graphql-yoga@5.21.2(graphql@16.14.2):
+ dependencies:
+ '@envelop/core': 5.5.1
+ '@envelop/instrumentation': 1.0.0
+ '@graphql-tools/executor': 1.5.3(graphql@16.14.2)
+ '@graphql-tools/schema': 10.0.33(graphql@16.14.2)
+ '@graphql-tools/utils': 10.11.0(graphql@16.14.2)
+ '@graphql-yoga/logger': 2.0.1
+ '@graphql-yoga/subscription': 5.0.5
+ '@whatwg-node/fetch': 0.10.13
+ '@whatwg-node/promise-helpers': 1.3.2
+ '@whatwg-node/server': 0.11.0
+ graphql: 16.14.2
+ lru-cache: 10.4.3
+ tslib: 2.8.1
+
+ graphql@16.14.2: {}
+
hookable@6.1.1: {}
import-without-cache@0.4.0: {}
+ jiti@2.7.0: {}
+
+ jsbi@4.3.2: {}
+
jsesc@3.1.0: {}
+ lru-cache@10.4.3: {}
+
obug@2.1.3: {}
pathe@2.0.3: {}
+ pg-cloudflare@1.4.0:
+ optional: true
+
+ pg-connection-string@2.13.0: {}
+
+ pg-int8@1.0.1: {}
+
+ pg-pool@3.14.0(pg@8.21.0):
+ dependencies:
+ pg: 8.21.0
+
+ pg-protocol@1.14.0: {}
+
+ pg-types@2.2.0:
+ dependencies:
+ pg-int8: 1.0.1
+ postgres-array: 2.0.0
+ postgres-bytea: 1.0.1
+ postgres-date: 1.0.7
+ postgres-interval: 1.2.0
+
+ pg@8.21.0:
+ dependencies:
+ pg-connection-string: 2.13.0
+ pg-pool: 3.14.0(pg@8.21.0)
+ pg-protocol: 1.14.0
+ pg-types: 2.2.0
+ pgpass: 1.0.5
+ optionalDependencies:
+ pg-cloudflare: 1.4.0
+
+ pgpass@1.0.5:
+ dependencies:
+ split2: 4.2.0
+
picomatch@4.0.4: {}
+ postgres-array@2.0.0: {}
+
+ postgres-bytea@1.0.1: {}
+
+ postgres-date@1.0.7: {}
+
+ postgres-interval@1.2.0:
+ dependencies:
+ xtend: 4.0.2
+
quansync@1.0.0: {}
resolve-pkg-maps@1.0.0: {}
- rolldown-plugin-dts@0.26.0(rolldown@1.1.1):
+ rolldown-plugin-dts@0.26.0(rolldown@1.1.1)(typescript@6.0.3):
dependencies:
'@babel/generator': 8.0.0
'@babel/helper-validator-identifier': 8.0.0
@@ -523,6 +1508,8 @@ snapshots:
get-tsconfig: 5.0.0-beta.5
obug: 2.1.3
rolldown: 1.1.1
+ optionalDependencies:
+ typescript: 6.0.3
transitivePeerDependencies:
- oxc-resolver
@@ -549,6 +1536,10 @@ snapshots:
semver@7.8.4: {}
+ split2@4.2.0: {}
+
+ srvx@0.11.16: {}
+
tinyexec@1.2.4: {}
tinyglobby@0.2.17:
@@ -558,7 +1549,7 @@ snapshots:
tree-kill@1.2.2: {}
- tsdown@0.22.3:
+ tsdown@0.22.3(typescript@6.0.3):
dependencies:
ansis: 4.3.1
cac: 7.0.0
@@ -569,22 +1560,31 @@ snapshots:
obug: 2.1.3
picomatch: 4.0.4
rolldown: 1.1.1
- rolldown-plugin-dts: 0.26.0(rolldown@1.1.1)
+ rolldown-plugin-dts: 0.26.0(rolldown@1.1.1)(typescript@6.0.3)
semver: 7.8.4
tinyexec: 1.2.4
tinyglobby: 0.2.17
tree-kill: 1.2.2
unconfig-core: 7.5.0
+ optionalDependencies:
+ typescript: 6.0.3
transitivePeerDependencies:
- '@ts-macro/tsc'
- '@typescript/native-preview'
- oxc-resolver
- vue-tsc
- tslib@2.8.1:
- optional: true
+ tslib@2.8.1: {}
+
+ typescript@6.0.3: {}
unconfig-core@7.5.0:
dependencies:
'@quansync/fs': 1.0.0
quansync: 1.0.0
+
+ undici-types@8.3.0: {}
+
+ urlpattern-polyfill@10.1.0: {}
+
+ xtend@4.0.2: {}
diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml
index 76f36d4..b4b4756 100644
--- a/pnpm-workspace.yaml
+++ b/pnpm-workspace.yaml
@@ -1,5 +1,11 @@
packages:
- packages/*
+allowBuilds:
+ esbuild: true
+
catalog:
+ "@types/node": ^26.0.0
+ drizzle-orm: 1.0.0-beta.22
tsdown: ^0.22.3
+ typescript: ^6.0.3
diff --git a/scripts/dev.mts b/scripts/dev.mts
new file mode 100644
index 0000000..29cd906
--- /dev/null
+++ b/scripts/dev.mts
@@ -0,0 +1,235 @@
+// DrFed: A web-based platform for developing and debugging ActivityPub apps
+// Copyright (C) 2026 DrFed team
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see .
+import { spawn, type ChildProcess } from "node:child_process";
+import { existsSync } from "node:fs";
+import { readdir, rm } from "node:fs/promises";
+import { dirname, join } from "node:path";
+import process from "node:process";
+import { fileURLToPath } from "node:url";
+
+interface ExitResult {
+ readonly code: number | null;
+ readonly signal: NodeJS.Signals | null;
+ readonly error?: Error;
+}
+
+interface ShutdownOptions {
+ readonly skipServer?: boolean;
+}
+
+const root = join(dirname(fileURLToPath(import.meta.url)), "..");
+const packagesDir = join(root, "packages");
+const isWindows = process.platform === "win32";
+const pnpm = isWindows ? "pnpm.cmd" : "pnpm";
+
+let buildProcess: ChildProcess | undefined;
+let serverProcess: ChildProcess | undefined;
+let shuttingDown = false;
+
+process.on("SIGINT", () => {
+ void shutdown(0, "SIGINT");
+});
+process.on("SIGTERM", () => {
+ void shutdown(143, "SIGTERM");
+});
+
+try {
+ await removeDistDirs();
+
+ buildProcess = spawnManaged(
+ pnpm,
+ ["--parallel", "--recursive", "exec", "tsdown", "--watch", "--no-clean"],
+ root,
+ );
+ const buildExit = waitForExit(buildProcess);
+
+ await waitForBuilds(buildExit);
+
+ serverProcess = spawnManaged(
+ process.execPath,
+ ["--watch", "bin/drfed-server.mjs", "--pglite-data-path", "../../.pgdata"],
+ join(root, "packages", "drfed"),
+ );
+ const serverExit = await waitForExit(serverProcess);
+ if (serverExit.error != null) throw serverExit.error;
+ const exitCode = serverExit.code ?? signalExitCode(serverExit.signal) ?? 1;
+ await shutdown(exitCode, "SIGTERM", { skipServer: true });
+} catch (error) {
+ console.error(error instanceof Error ? error.message : error);
+ await shutdown(1, "SIGTERM");
+}
+
+async function removeDistDirs() {
+ const packages = await readdir(packagesDir, { withFileTypes: true });
+ await Promise.all(
+ packages
+ .filter((entry) => entry.isDirectory())
+ .map((entry) =>
+ rm(join(packagesDir, entry.name, "dist"), {
+ force: true,
+ recursive: true,
+ }),
+ ),
+ );
+}
+
+function spawnManaged(
+ command: string,
+ args: readonly string[],
+ cwd: string,
+): ChildProcess {
+ return spawn(command, args, {
+ cwd,
+ detached: !isWindows,
+ env: process.env,
+ stdio: "inherit",
+ windowsHide: true,
+ });
+}
+
+async function waitForBuilds(buildExit: Promise): Promise {
+ let buildExitResult: ExitResult | undefined;
+ buildExit.then((result) => {
+ buildExitResult = result;
+ });
+
+ while (true) {
+ if (buildExitResult != null) {
+ if (buildExitResult.error != null) throw buildExitResult.error;
+ const exitCode =
+ buildExitResult.code ?? signalExitCode(buildExitResult.signal);
+ throw new Error(
+ `Build watcher exited before startup completed: ${exitCode}`,
+ );
+ }
+
+ const packages = await readdir(packagesDir, { withFileTypes: true });
+ const allGenerated = await Promise.all(
+ packages
+ .filter((entry) => entry.isDirectory())
+ .map(async (entry) => {
+ const distDir = join(packagesDir, entry.name, "dist");
+ if (!existsSync(distDir)) return false;
+ const entries = await readdir(distDir);
+ return entries.length > 0;
+ }),
+ );
+ if (allGenerated.every(Boolean)) return;
+
+ await sleep(100);
+ }
+}
+
+async function shutdown(
+ exitCode: number,
+ signal: NodeJS.Signals,
+ options: ShutdownOptions = {},
+): Promise {
+ if (shuttingDown) {
+ forceKill(serverProcess);
+ forceKill(buildProcess);
+ process.exit(exitCode);
+ }
+ shuttingDown = true;
+
+ await Promise.all([
+ options.skipServer
+ ? Promise.resolve()
+ : terminate(serverProcess, signal === "SIGINT" ? "SIGINT" : "SIGTERM"),
+ terminate(buildProcess, "SIGTERM"),
+ ]);
+ process.exit(exitCode);
+}
+
+function waitForExit(child: ChildProcess): Promise {
+ return new Promise((resolve) => {
+ child.once("exit", (code, signal) => resolve({ code, signal }));
+ child.once("error", (error) => resolve({ code: 1, signal: null, error }));
+ });
+}
+
+function terminate(
+ child: ChildProcess | undefined,
+ signal: NodeJS.Signals,
+): Promise {
+ if (child == null || child.exitCode != null || child.signalCode != null) {
+ return Promise.resolve();
+ }
+
+ return new Promise((resolve) => {
+ const timeout = setTimeout(() => {
+ forceKill(child);
+ }, 5000);
+
+ child.once("exit", () => {
+ clearTimeout(timeout);
+ resolve();
+ });
+ killTree(child, signal);
+ });
+}
+
+function killTree(child: ChildProcess, signal: NodeJS.Signals): void {
+ try {
+ if (isWindows) {
+ child.kill(signal);
+ } else {
+ if (child.pid != null) process.kill(-child.pid, signal);
+ }
+ } catch (error) {
+ if (!isProcessLookupError(error)) child.kill(signal);
+ }
+}
+
+function forceKill(child: ChildProcess | undefined): void {
+ if (child == null || child.exitCode != null || child.signalCode != null)
+ return;
+
+ if (isWindows) {
+ spawn("taskkill", ["/pid", String(child.pid), "/t", "/f"], {
+ stdio: "ignore",
+ windowsHide: true,
+ });
+ return;
+ }
+
+ try {
+ if (child.pid != null) process.kill(-child.pid, "SIGKILL");
+ } catch (error) {
+ if (!isProcessLookupError(error)) child.kill("SIGKILL");
+ }
+}
+
+function isProcessLookupError(error: unknown): boolean {
+ return (
+ typeof error === "object" &&
+ error != null &&
+ "code" in error &&
+ error.code === "ESRCH"
+ );
+}
+
+function signalExitCode(signal: NodeJS.Signals | null): number | undefined {
+ if (signal === "SIGINT") return 130;
+ if (signal === "SIGTERM") return 143;
+ return undefined;
+}
+
+function sleep(ms: number): Promise {
+ return new Promise((resolve) => {
+ setTimeout(resolve, ms);
+ });
+}