Zachary Lowden 165da40294 feat(app-blocks): author-fee viewer-charge path (slice 2b, dark) (#4958)
* feat(app-blocks): slice 2b base — the author-fee settlement rail, rebased onto the merged ledger

Slice 2a (the accrual ledger) merged as dce428a492 via squash, so the previous
slice2b branch re-proposed the entire ledger against main. Rebuilt off main so
this branch carries ONLY the settlement delta: 4 files, +1016.

The six audit rounds behind this code are preserved on
zach/app-blocks-author-fee-audit-archive (7c2a38565f); GitHub auto-deleted the
2a branch on merge, which briefly made slice2b their only ref.

Still dark: nothing calls accrueBlockAuthorFee on main, the Flipt flag
app-blocks-author-fee-enabled is false, and the cron is registered in the job
array but not with the external scheduler.

* feat(app-blocks): author-fee viewer-charge path (slice 2b, dark)

Slice 1 computed the per-generation author fee and threw it away. Slice 2a
persisted an accrual ledger and a settlement rail with ZERO callers. This slice
is the caller: it prices the fee, debits the viewer, writes the accrual and
reverses both when the generation does not survive.

THE FEE IS PRICED INTO THE RESERVATION, NOT DEBITED OUTSIDE IT. This is the one
safety hole the design review found. The submit order on every path is
whatIf -> budget gate + reservations -> submitWorkflow -> charge. A fee debited
at the end would escape all five guardrails those reservations implement: the
token per-call budget, the per-user daily cap, the viewer's own per-app consent
budget, the per-app aggregate cap and the dev-tunnel backstop. The consent
budget in particular would then bound the part of the price the app does NOT
set while leaving the part it DOES set unbounded.

So the path splits in two. quoteBlockAuthorFee runs against the whatIf response
and its feeBuzz is folded into the number every gate and reservation reads.
chargeBlockAuthorFee runs against the realized submit response and takes the
reserved amount as a HARD CEILING - charged = min(reserved, realized). A path
that reserved nothing passes 0 and can then charge nothing, whatever the
realized base says, so the property is structural rather than conventional.

Two of the four submit paths are in that state deliberately: customComfy takes
no whatIf quote at all (its ceiling IS the declared maxBuzz) and the
pass-through quote helper returns a total only. Neither has a pre-submit
cost.base to price a fee from. Both still call the charge with 0 so the
population of submit paths stays closed and the seam guard can enumerate it.

SELF-DEALING IS READ BEFORE THE DEBIT. Slice 2a implemented the exclusion inside
the accrual writer and left a note saying a charge path must call it before the
debit or extract it. It is extracted - resolveBlockAuthorFeePayee - and the
quote calls it, so a self-dealing generation never even has a fee reserved. The
accrual still calls it as the write-side belt for any other caller. The owner
read stays in the file the ownership-gate ledger already enumerates.

THE FEE FOLLOWS THE GENERATION'S REFUND. pollWorkflow and cancelAppWorkflow
reverse on any non-succeeded terminal status: the debit is refunded and the
unsettled accrual row deleted. The DELETE is status-guarded, which is both the
settled-row refusal and the lock - only the caller whose delete matched issues
the refund, so concurrent terminal polls cannot double-refund. A SETTLED row is
refused rather than clawed back. The succeeded guard matters most on cancel,
which races completion.

Reconciled BY COUNT throughout: createBuzzTransactionMany does not throw on a
per-transaction failure, it drops the result from both arrays. A landed debit
whose accrual then failed is refunded under the reversal key, so a later
terminal reversal conflicts instead of paying twice.

NO MIGRATION. There is no clawed_back status: the status CHECK is constrained to
('accrued','settled'), every migration here is applied by hand per environment,
and a status-guarded DELETE is the atomic claim the reversal needs anyway. The
consequence is stated in the code - a reversed generation leaves no row, so the
reversal count lives only in the block-author-fee Axiom stream.

STILL DARK. Every entry point is behind app-blocks-author-fee-enabled, which is
false. With the flag off no fee is quoted, so nothing is reserved, so the clamp
makes a charge impossible and the ledger stays empty.

Also folds the txt2img spend-basis derivation into the shared
deriveBlockSpendBasis the other three paths already used - the fee and the
attribution must agree on the currency, and the inline copy was unreachable
from where the fee needed the answer. The attribution fallback keeps reading
the orchestrator's own quoted total, not the fee-inclusive cost.

TESTS
- src/server/services/__tests__/no-divergent-author-fee-base.test.ts gains 9
  guards over the router call sites. Measured at dce428a492: 7 RED, 2 VACUOUSLY
  GREEN. The two green ones iterate the (empty) call-site array, so they assert
  nothing there; what stops that being a hole is the positive control and the
  count assertion in the same describe, both of which are red. This is the
  regression coverage for the change.
- author-fee-charge.service.test.ts is 29 NEW-MODULE tests, not regression
  coverage - at the base they cannot run at all because the module does not
  exist. What makes them real is the mutation sweep: 21 counted mutants, all 21
  killed, each by its own named assertion. A 22nd was RETRACTED rather than
  reported as a survivor - it inserted a self-dealing branch AFTER
  resolveBlockAuthorFeePayee had already returned, so it was unreachable and
  proved nothing either way. Its replacements break the real predicate
  (M4a/M4b) and kill the charge-path test, which is what proves the charge
  actually reaches the shared predicate rather than merely importing it.
- Prisma is mocked at the module boundary in every one of them, so none of this
  is a claim about behaviour against a real database.

* fix(app-blocks): make the author-fee charge total and the reversal guard structural

Two rounds of adversarial audit on the slice-2b viewer-charge path. Both
blockers moved money in a direction D1 forbids; the rest are guards that read
as coverage while providing none, and comments the change falsified.

BLOCKER 1 — chargeBlockAuthorFee was not total.
The accrueBlockAuthorFee call sat outside every try. It reaches
resolveBlockAuthorFeePayee, whose dbRead.oauthClient.findUnique is documented
to PROPAGATE, and by that point the viewer's debit has already landed. The
rejection escaped to the router: viewer charged, no accrual row, no refund,
and the generation surfaced as an error the viewer had nonetheless paid for.
Wrapped, routed into the same refund branch reason 'error' already takes, and
the two comments asserting the false property now describe what the code does.

BLOCKER 2 — the "already settled" guard was spelled on status.
Settlement MINTS at createBuzzTransactionMany and only flips status at the
updateMany after it, so between those statements the money is the author's
while the row still reads accrued. The reachable arm is not that race: when
the flip throws, flipFailures is incremented and rows stay accrued until the
next nightly run, so every reversal in that ~24h window deterministically
double-paid. The guard is now the settlement rail's OWN eligibility boundary,
exported as one predicate and read by both sides: a row accrued in the current
UTC day is invisible to every settlement run, so no mint can have been
attempted for it. Re-asserted in the DELETE so it is a CLAIM, not a check.
NO SCHEMA CHANGE — settlement_key is forbidden on an unsettled row by
block_author_fee_accrual_settled_key_check, so the obvious pre-mint claim
column is unavailable without a hand-applied migration.

Also in this batch:

- cancelAppWorkflow issues a real orchestrator cancel and reversed NOTHING.
  Three artefacts named it as one of the two reversing observers; it was not
  one of them. A block cancelling a queued generation through it got the
  generation refunded while the accrual stood and the nightly job minted the
  fee. Wired, and the seam guard now derives its population from the CANCEL
  sites instead of counting the reversal sites that happen to exist.
- Both cancel paths guarded only on status !== 'succeeded'. cancelWorkflow
  resolves on a non-2xx PATCH, so a failed cancel re-read as 'processing' and
  reversed anyway: viewer keeps the generation AND the fee. All three
  observers now spell the same terminal + not-succeeded guard.
- The 'whatif' sentinel was not excluded at the charge guards. Under it the
  charge key and the UNIQUE accrual row are shared across every viewer, so a
  second such generation conflicts, reports charged:true having debited
  nothing, and a later reversal refunds the wrong viewer.
- persistCustomComfySettle was handed the fee-inclusive ceiling while
  settleCustomComfySpend refunds against the generation-only cost.
- Deleted the two dead chargeBlockAuthorFee calls that returned
  'not-reserved' at the callee's first statement, and the two
  deriveBlockSpendBasis hoists that moved onto the awaited request path only
  to feed them. The population stays closed via a NO_FEE_PATHS ledger that
  fails on growth AND shrink, following no-unguarded-billable-submit.
- Three mutants that survived the full suite are now pinned: the charge's
  buzzType, capOverage's comparand, and the charge being awaited.
- The settlement flip's own status guard had no killing mutation; its where
  clause is now asserted.
- Corrected the claims the change falsified: "every entry point is behind the
  flag" (reverseBlockAuthorFee reads none, deliberately), the capOverage
  comparand, and the unknown-`variable` tri-state, which slice 2b decides.

Tests: 83 across the four author-fee suites, up from 75. Nine are RED at
1672a1e3cc, each with its own assertion. Everything mocks Prisma and the Buzz
service at the module boundary — no claim is made about real-database or
live-Buzz behaviour.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* test(app-blocks): pin every fee reversal to its own procedure input id

The reversal deletes a row and issues a refund, and all three observing
procedures have a snapshot or projection in scope carrying a different
workflow id, so a site keyed on the wrong one refunds the wrong viewer.
Mutation-checked: keying cancelAppWorkflow on canceledWorkflow.workflowId
fails "EVERY procedure that cancels a workflow also reverses the fee" with
`expected ... to contain 'workflowId: input.workflowId'`.

terminalStatus is deliberately not pinned to one expression — the poll and
cancelWorkflow read snapshot.status, cancelAppWorkflow reads its projection.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* docs(app-blocks): make the author-fee comments say what the code does

Round 2 of the slice-2b delta audit. Every finding but one is a FALSE OR
MISLEADING CLAIM rather than a logic bug: the code is largely right and the
comments, docblocks and PR body describing it are not. So this round changes
words, not behaviour — with one exception, which is a test.

BLOCKING — the documented reversibility window was 2.5h wider than the code's.
Three artefacts said a fee accrued on day D is reversible "until the settlement
job runs at 02:30 UTC on D+1". The guard is isSettlementEligible(accruedAt, now)
= accruedAt < utcDayStart(now), so reversibility ends at 00:00 UTC on D+1 and
the job's schedule is irrelevant to it. Both documents already stated it
correctly elsewhere and contradicted themselves. Corrected everywhere, with the
worked case a reader has to get right: accrued 2026-09-18T14:00Z, polled failed
at 2026-09-19T01:00Z is REFUSED — nothing has minted and the fee is not
reversible.

The accruedAt re-assertion in the reversal deleteMany is INERT, and two comments
claimed it closes a race it cannot. `now` is frozen before the read and
accrued_at is written once by the column default and never updated, so the
boundary comparison has the same answer at the check and at the claim. `status`
can move between the two statements; accruedAt cannot. The clause is KEPT
(harmless defence-in-depth, and removing it would churn the WHERE-shape pin) and
both comments now say what is true: the row is safe because the settlement scan
cannot see it at all, `status` is the clause that closes a real race, accruedAt
is belt-and-braces made redundant by the frozen clock.

The unknown-`variable` tri-state was motivated by the one case where its own
claim fails. "A viewer charged for a cap-priced generation is one reversal" is
false: a cap price is what the orchestrator prorates, proration happens on
`succeeded`, and all three observers gate on status !== 'succeeded' — so there
is no reversal at all for that case, as reverseBlockAuthorFee's own docblock
says. The behaviour is UNCHANGED (`=== true`, charge on unknown); the
justification now rests on visibility, and the residue paragraph says the fee is
NOT reversible when the generation succeeds. Cap-priced generations were
measured at 0.94% of App Blocks traffic (5 of 532, all one app).

"byte-restored to dce428a492" was FALSE, and the difference is an undeclared
behaviour change. submitCustomComfyWorkflow and submitPassThroughStepWorkflow
are code-identical to the base except one hunk each: the guard gained
spendWorkflowId !== 'whatif'. Those paths charge no author fee, so the whatif
rationale (shared Buzz idempotency key, UNIQUE accrual row) does not apply; the
only consumer inside those blocks is recordSpendAttribution, so the delta stops
writing spend-attribution rows for submits whose orchestrator response carries
no workflow id. The exclusion is KEPT — a shared 'whatif' attribution key has
the same cross-viewer collision shape — and is now DECLARED at both sites,
ledgered in NO_FEE_PATHS with its own reason, and pinned by a test rather than
inherited from a loop over all four submit markers.

Also:

- reason 'already-settled' is unreachable in production: the eligibility guard
  precedes the status guard and the only writer of status='settled' can only
  touch rows eligibility already refuses, so it needs app-clock skew across
  midnight (or a manual settlement run with a future date). An operator alerting
  on that log line would get permanent silence. The docblock presented it as an
  ordinary operational distinction and a test comment named the wrong mechanism
  ("only if a settlement run is mid-flight" — mid-flight is not sufficient).
- "9 RED at 1672a1e3cc, each on its own assertion" was false for 1 of 9: the
  accrual-threw refund test fails with Error: connection lost, the rejection
  escaping, not an assertion. The test's own comment said so; the claim did not.
- author-fee-accrual.service.ts said every MONEY-MOVING entry point is behind
  the flag with reverseBlockAuthorFee as "the exception". It moves money — it
  refunds via createBuzzTransactionMany. Accurate form: every entry point that
  can CREATE an obligation is behind the flag, and the reversal deliberately is
  not. Also corrected "on every terminal poll": it does not fire on succeeded.
- settleBlockAuthorFees({ date }) is a public parameter and the disjointness
  property holds only while no caller passes a date AHEAD of the reverser's
  clock. The single production caller is correct; the precondition is now
  documented on settlementBoundary and on the field.
- Two nits: the eligibility log line no longer defends against a non-Date
  accruedAt one line after isSettlementEligible called .getTime() on it
  unguarded, and the count < 1 branch says why it shares reason 'no-accrual'.

Tests: 84 across the four author-fee suites, up from 83 at 29ee5a8a42. The one
new test is RED at 29ee5a8a42 (1 failed / 20 passed in that file) on its own
declaration assertion, GREEN at head; its structural half was shown reachable by
dropping the whatif clause from the pass-through guard. Everything continues to
mock Prisma and the Buzz service at the module boundary — no claim is made about
real-database or live-Buzz behaviour.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* docs(app-blocks): attach the no-accrual note to the result type, not to `reason`

Prettier hoisted the union first member's docblock onto the `reason` property, so a comment documenting ONE member read as documenting the whole field. Moved to the type's own docblock; no other change.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* test(app-blocks): give the whatif-declaration guard a positive control

Every assertion in it sits inside a loop over NO_FEE_PATHS, so an emptied ledger would make it pass having checked nothing. The neighbouring ledger test would also go red in that case, which is exactly why the control is explicit: a guard that only fails because a DIFFERENT guard fails is green for the wrong reason. Verified by emptying the ledger - this test now fails on its own assertion (expected [] to deeply equal [submitCustomComfyWorkflow, submitPassThroughStepWorkflow]).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* docs(app-blocks): correct three overstated claims in the author-fee comments

Round 3 found three prose defects and zero behaviour defects. This pass fixes
exactly those three and changes no behaviour: the only executable edit is the
pinned declaration string the whatif-attribution guard asserts, which is updated
so the test pins the true claim rather than the old overstatement.

F1 — author-fee-accrual.service.ts. The dark-path cost sentence said the
findUnique fires on every terminal non-succeeded observation "plus every
cancel", contradicting its own preceding clause: two of the three observers ARE
the cancel paths (blocks.router.ts:4173 cancelWorkflow, :4399 cancelAppWorkflow)
and both carry the identical compound guard. A reader sizing DB cost from this
counted cancels that issue no query. Now says the two cancel paths carry that
same compound guard rather than calling unconditionally.

F2 — author-fee-charge.service.ts + its test. Both said reaching the
already-settled arm needs the settling app's clock "a whole UTC day ahead" of
the reversing app's, and concluded an operator alerting on its log line "would
get permanent silence". The real condition is
utcDayStart(T_reverser) <= accruedAt < utcDayStart(T_settler): the two clocks
only have to land on DIFFERENT UTC days, not 24h apart. Against the 02:30 UTC
schedule ('30 2 * * *' in jobs/settle-block-author-fees.ts) a ~2.5h lag on the
reversing app's clock suffices, and in the abstract a sub-second straddle of
midnight does — an ordinary NTP failure, not an exotic one. Softened to: the arm
is unreachable under CORRECT clocks, so the line should be near-silent, and if
it fires clock skew is the first thing to suspect rather than the thing ruled
out. Same class of error the previous round fixed — a stated bound wider than
the code's.

F3 — the 'whatif' premise was false and was asserted in five places. Settled by
reading the orchestrator's own source: a real submit can never return a workflow
without an id, and neither can a whatIf. The id is minted before any whatif
branch and stamped twice, in WorkflowsController and again unconditionally in
WorkflowGrain.TryInitializeAsync, which runs BEFORE the EstimateOnly early
return; both response arms return that same grain object. So workflow.id is not
observed absent on any path today, and the exclusion is a no-op defensive guard
rather than an active behaviour change — no attribution row is dropped.

The guard is KEPT: Id is string?, the OpenAPI required list omits id, and
DefaultIgnoreCondition=WhenWritingNull is set globally, so a future regression
would make the field silently vanish rather than error. Only the declared
rationale changes, at all five sites — workflow.service.ts (which was right that
submit carries a real id but wrong in its own premise that a whatIf has none),
both blocks.router.ts comments, and the two NO_FEE_PATHS ledger entries plus the
test's rationale.

The WHATIF_ATTRIBUTION_DECLARATION pin is updated in step so it pins the true
claim. Pin liveness verified in both directions: updating the comments without
the string fails naming submitCustomComfyWorkflow, and updating the string with
one comment left stale fails naming submitPassThroughStepWorkflow — the pin is
live, not decorative.

Tests: 84 passing across the four author-fee suites, unchanged from the baseline
at e1706457e7 (this adds no coverage). Formatted with the repo's pinned prettier
2.8.8.

* docs(app-blocks): retire the whatif-has-no-id premise from its sixth and last site

The round-3 wording pass corrected five sites asserting that a whatif/estimate
workflow carries no orchestrator id. This test's name and comment were the
sixth and were left contradicting the other five.

Settled by reading the orchestrator source: the id is minted before any whatif
branch and stamped twice, the second time in WorkflowGrain.TryInitializeAsync
from the grain key, before the estimate-only early return. Both response arms
return that same object. So the fallback is currently unreachable and this test
pins a defensive floor against a silently-omitted field, not an observed case.

Name and comment only; the fixture, the assertions and the guard are unchanged.
178 passed in this file, prettier 2.8.8 clean.

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-18 21:06:38 -05:00

Contributors Forks Stargazers Issues Apache License 2.0 Discord


Table of Contents

About the Project

Our goal with this project is to create a platform where people can share their stable diffusion models (textual inversions, hypernetworks, aesthetic gradients, VAEs, and any other crazy stuff people do to customize their AI generations), collaborate with others to improve them, and learn from each other's work. The platform allows users to create an account, upload their models, and browse models that have been shared by others. Users can also leave comments and feedback on each other's models to facilitate collaboration and knowledge sharing.

Tech Stack

We've built this project using a combination of modern web technologies, including Next.js for the frontend, TRPC for the API, and Prisma + Postgres for the database. By leveraging these tools, we've been able to create a scalable and maintainable platform that is both user-friendly and powerful.

  • DB: Prisma + Postgres
  • API: tRPC
  • Front-end + Back-end: NextJS
  • UI Kit: Mantine
  • Storage: Cloudflare

Getting Started

To get a local copy up and running, follow these steps.

Prerequisites

  • Docker, with Compose v2 (docker compose, not the retired hyphenated docker-compose). The database, Redis, MinIO, Meilisearch, ClickHouse and the mail catcher all run as containers.
  • Node.js 24.19.0. Not "20 or later" — package.json declares engines.node: ">=24.0.0 <25". The exact version lives in .nvmrc; CI installs that file's version and the production image is built on the same one, so nvm use (or any tool that reads .nvmrc) is the right way to get it. Note that nothing stops you: pnpm install only prints WARN Unsupported engine and carries on, so the wrong major surfaces later as odd test failures rather than as a refusal at install time.
  • pnpm. This repo is pnpm-only, and this one is enforced — npm install exits 1 via the preinstall only-allow pnpm hook. corepack enable will pick up the packageManager field for you.
  • Make (optional).

Installation

Standard setup

git clone https://github.com/civitai/civitai.git
cd civitai
nvm use                                              # reads .nvmrc -> 24.19.0
corepack enable
git submodule update --init event-engine-common
cp .env-example .env.development
docker compose -f docker-compose.base.yml up -d
pnpm install
pnpm dev

Optional: Nix flake

Optional, and not the supported default. The standard setup above is what the project expects and what CI builds; nothing in the repo requires Nix, and you can ignore this section entirely. It exists because NixOS cannot use Prisma's published engines (there is no linux-nixos build), so a flake is the practical way to work on this repo there. If you are not on NixOS and not already a flakes user, skip it.

The flake owns the toolchain, so you do not install Node or pnpm yourself:

git clone https://github.com/civitai/civitai.git
cd civitai
nix run .#dev

That single command checks Docker is usable, checks out the event-engine-common submodule, creates .env.development from .env-example if you do not already have one, starts the container stack, waits for Postgres, runs pnpm install, and then starts the dev server on http://localhost:3000. Every step is idempotent — it is safe to re-run in a checkout that already works, and it will not overwrite your .env.development or touch your data.

Useful variants:

nix run .#dev -- --no-start   # bootstrap only, leave the services running
nix run .#dev -- --full       # also start the signals/buzz containers (see below)
nix run .#doctor              # check the flake's pins against the repo
nix flake check               # the same checks, plus their own self-test

For an interactive shell with the same toolchain, use nix develop, or copy .envrc.example to .envrc and run direnv allow to get it automatically on cd.

With devcontainers

⚠️ Known out of step: .devcontainer/public/docker-compose.yml pins mcr.microsoft.com/devcontainers/typescript-node:1-22, i.e. Node 22, which is outside this repo's engines.node range. pnpm install will warn rather than stop, so the container comes up and then misbehaves in ways that look like your branch. There is no 1-24 tag (the template major moved on); 3-24 is the closest equivalent. Not changed here because it could not be exercised.

⚠️ Important Warning for Windows Users: Either clone this repo onto a WSL volume, or use the "clone repository in named container volume" command. Otherwise, you will see performance issues.

  • Open the directory up in your IDE of choice
    • VS Code should prompt you to "Open in container"
      • If not, you may need to manually run Dev Containers: Open Folder in Container
    • For other IDEs, you may need to open the .devcontainer/devcontainer.json file, and click "Create devcontainer and mount sources"
    • Note: this may take some time to run initially
  • Run make run

The signals and buzz services

docker-compose.base.yml holds everything a contributor needs (and is also what nix run .#dev starts). The extra services in docker-compose.yml (signals, buzz) come from private ghcr.io images, so they only work for internal members:

  • create a GitHub personal access token with read:packages
  • set it as CR_PAT
  • echo $CR_PAT | docker login ghcr.io -u USERNAME --password-stdin
  • then docker compose up -d (or, with the flake, nix run .#dev -- --full)

After the first start

  1. Edit .env.development. Most defaults work out of the box; these do not:
    • S3 upload credentials. Open the MinIO console at http://localhost:9001 (username and password both minioadmin) — note it is port 9001, port 9000 is the S3 API itself — go to "Access Keys", click "Create Access Key", and copy the key and secret into S3_UPLOAD_KEY / S3_UPLOAD_SECRET and S3_IMAGE_UPLOAD_KEY / S3_IMAGE_UPLOAD_SECRET.
    • WEBHOOK_TOKEN — any random string; it authenticates requests to the webhook endpoint.
    • EMAIL_USER, EMAIL_PASS, and EMAIL_FROM (a valid email format) — any values, but they must be set for user registration to work.
  2. On an empty database, populate it. These are slow and destructive, which is why no bootstrap runs them for you:
    make run-migrations
    make reseed
    
  3. Visit http://localhost:3000.

Please report any issues with these commands to us on discord.

* Note that account creation will run emails through maildev, which can be accessed at http://localhost:1080.

Altering your user

  • First, create an account for yourself as you normally would through the UI.
  • You may wish to set yourself up as a moderator. To do so:
    • Use a database editor (like DataGrip) or connect directly to the DB (PGPASSWORD=postgres psql -h localhost -p 15432 -U postgres civitai)
    • Find your user (by email or username), and change isModerator to true

Known limitations

Services that require external input will currently not work locally. These include:

  • Orchestration (Generation, Training)
  • Signals (Chat, Notifications, other real-time updates)
  • Buzz

Contributing

Any contributions you make are greatly appreciated.

If you have a suggestion that would make this better, please fork the repo and create a pull request. You can also simply open an issue with the tag "enhancement". Don't forget to give the project a star! Thanks again!

  1. Fork the repository to your own GitHub account.
  2. Create a new branch for your changes.
  3. Make your changes to the code.
  4. Commit your changes and push the branch to your forked repository.
  5. Open a pull request on our repository.

If you would like to be more involved, consider joining the Community Development Team! For more information on the team as well as how to join, see Calling All Developers: Join Civitai's Community Development Team.

Data Migrations

Over the course of development, you may need to change the structure of the database. To do this:

  1. Make your changes to the packages/civitai-db-schema/prisma/schema.full.prisma file. Not schema.prisma — that one is gitignored and regenerated from schema.full.prisma by scripts/generate-slim-schema.js on every pnpm run db:generate, so edits to it are silently overwritten.
  2. Run pnpm run db:migrate:empty "brief description here". This creates packages/civitai-db-schema/prisma/migrations/YYYYMMDDHHmmss_brief_description_here/migration.sql for you, in the one directory Prisma reads. To create it by hand instead, use that same path — not the prisma/migrations directory at the repo root, which predates the monorepo layout and is no longer read.
  3. Put your sql changes in the generated migration.sql
    • These are usually simple sql commands like ALTER TABLE ...
  4. Run make run-migrations and make gen-prisma
  5. If you are adding/changing a column or table, please try to keep the gen_seed.ts file up to date with these changes.

Sponsors

Support this project by becoming a sponsor. Your logo will show up here with a link to your website.

License

Apache License 2.0 - Please have a look at the LICENSE for more details.

S
Description
clickup: Interact with ClickUp tasks and documents - get task details, view comments, create and manage tasks, create and edit docs. Use when working with ClickUp…; quick-mockups: Create multiple UI design mockups in parallel. Use when asked to create mockups, wireframes, or design variations for a feature. Creates HTML files using…
Readme 362 MiB
Languages
TypeScript 93.3%
JavaScript 2.6%
Svelte 2.5%
PLpgSQL 0.5%
SCSS 0.4%
Other 0.6%