Qubit Notes is a study app. Students upload their study material, ask questions about it, and get answers that cite their sources. The assistant also keeps study sessions with memory, generates quizzes and flashcards, and schedules reviews.
The app needs nothing but its own database, a place for files and a model provider: by default it is its own platform. It can run on the gogi platform instead, with the gogi-python SDK.
backend/: FastAPI app. The domain talks to its platform only through theStudyPlatformport (backend/src/qubit_notes/platform/port.py).StandalonePlatform(platform/standalone/) implements it with the app's own database, files and model providers;GogiPlatformimplements it with gogi-python.frontend/: React + TypeScript (Vite). Students sign in by name (there is no authentication yet), manage their courses' material by subject and topic, and share courses with classmates.features/: Gherkin specs. They drive development of both the backend and the UI.
- Courses, subjects and topics. Organise material by course, subject and topic, created before there is any material for them, with names and titles of your own (Subjects and topics).
- Material. Upload Markdown, text, PDFs, EPUBs and videos (MP4, WebM), large files included; or write a topic's material in the editor, with a preview, and edit it later. Markdown material keeps its images.
- Lessons from a document. Upload a textbook as a PDF, an EPUB or Markdown to a subject, and get a topic of each of its chapters, with the chapter's text as its material (Lessons from a document).
- Papers. Keep the papers you read under My papers, in collections apart from your courses: each paper stays whole, with its title, authors, year, venue and DOI, and is read, noted and asked about on its own or with the rest of its collection (Papers).
- PDFs and EPUBs as Markdown. Uploaded PDFs and EPUBs are converted to Markdown to read, edit and search, and can be downloaded as they were or as they are now (PDFs and EPUBs).
- Reading. Read material section by section, with mathematics typeset by KaTeX, each topic's material in the order its owners set, and links on to the previous and next topic (Reading in order).
- References between topics. Refer to another topic, material or section with
[[...]]; references become links, broken ones are reported, and a topic's page shows where it is mentioned (References between topics). - Notes. Take notes on passages of text, in a note that opens by the passage selected, or at moments of a video; notes follow their passage when the material is edited.
- Asking the assistant. Ask about the material being read or the whole course and get answers that cite their sources, in conversations the assistant remembers, kept private to each student.
- Summaries and quizzes. Summaries and quizzes of 1 to 20 questions on a section, a document or a whole topic, saved, retaken, and marked out of date when the material changes.
- Flashcards and reviews. Decks of 1 to 30 flashcards on a section, a document or a topic, whose cards come back for review by spaced repetition (Flashcards and reviews).
- Generating material. Have the AI write revision notes, cheat sheets, combined notes or worked examples from chosen materials, citing them as references (Generating material with the AI).
- Sharing. Share a course with classmates as readers or co-owners, and stop sharing it.
- Accounts. Sign in with Amazon Cognito where deployed, and change your email and password under Settings › Account (Signing in with Amazon Cognito).
- Administration. Admins choose the AI's provider (Anthropic or OpenAI) and model, and give, replace and remove the app's keys; the bar shows the platform's health (The app's keys, The AI's model). Where the app has the keys of both providers, each student chooses which answers them.
- Platforms. Runs on its own (SQLite or Postgres with pgvector, local files or S3, in-app or hosted embedding models) or as an application of gogi; ships as a container image with Terraform for AWS.
make install # backend venv (uv), Playwright's Chromium and frontend packages
make serve # build the frontend and serve it with the backend on http://localhost:8001
make dev-backend # http://localhost:8001/api/health (gogi's chromadb takes 8000)
make dev-frontend # http://localhost:5173, forwards /api to the backend
make lint
make bdd # every scenario, on an in-memory platform
make bdd-ui # only the @ui scenarios, in headless Chromium
make bdd-standalone # the @platform and @standalone scenarios, on the app's own platform
make bdd-gogi # the @platform scenarios, against a running gogi gatewayEach capability starts as a .feature file in features/; its steps live in backend/tests/bdd/steps/.
make bdd runs the scenarios against FakePlatform, an in-memory platform that needs no gogi stack and no API keys;
the ones tagged @ui build the frontend, serve it from the backend and drive it with Playwright.
make bdd-standalone uses the app's database (SQLite, or QUBIT_NOTES_TEST_DATABASE_URL), files in a temporary
directory and its in-app embedding models, downloaded the first time, with a stand-in AI; make bdd-standalone-ai
uses Anthropic's (QUBIT_NOTES_ANTHROPIC_API_KEY), and --with-embedding-keys also runs the scenarios that embed
with Voyage AI and OpenAI. make bdd-gogi needs gogi-python (make install-gogi) and a gateway at
QUBIT_NOTES_GATEWAY_URL (default localhost:50051).
./start_local.sh does it all in one go, on the app's own platform: it reads settings such as
QUBIT_NOTES_ANTHROPIC_API_KEY from .env (gitignored) if there is one, uses the dev database (Postgres with
pgvector in the container qubit-notes-pgvector, on 127.0.0.1:5433) unless QUBIT_NOTES_DATABASE_URL is set,
starts that container if it's stopped, runs the migrations and then make serve; ./start_local.sh dev runs
make dev-backend and make dev-frontend together instead. The first time, it makes the app's secret
(QUBIT_NOTES_SECRET_KEY) into .env, and it makes you ($USER) the app's admin, so you can give the app a missing
key from the bar (see The app's keys).
The backend needs Python 3.12+. To run the app on gogi, make install-gogi also installs gogi-python from
../gogi-python in editable mode; build its protobuf code first (gogi-python/install.sh or
uv run python scripts/build_protos.py). gogi-python isn't one of the app's dependencies, so a plain uv sync
removes it again; uv run leaves it.
The app keeps what students make (notes, summaries, quizzes, conversations, topics) in its own database:
SQLite by default (backend/qubit-notes.db), Postgres where it's deployed:
export QUBIT_NOTES_DATABASE_URL=postgresql+psycopg://qubit:...@db.example.org:5432/qubit_notes
uv run qubit-notes migrate # a deployment runs it before the new version serves; the app also migrates when it startsThe tables are made and changed by the Alembic migrations in backend/src/qubit_notes/migrations/versions. A change
to storage.py needs one: from backend/, uv run alembic revision --autogenerate -m "...", then check what it
wrote. tests/unit/test_migrations.py fails if storage.py and the migrations differ; with
QUBIT_NOTES_TEST_POSTGRES_URL set it checks Postgres too, and QUBIT_NOTES_TEST_DATABASE_URL runs the scenarios
against a database of your choosing, as CI does with Postgres.
An uploaded PDF or EPUB is converted to Markdown in the background (features/converting.feature,
backend/src/qubit_notes/conversion/), and from then on the material is that Markdown: students read, edit, search,
summarise and quiz it as Markdown. Its pictures become its images.
- PDFs are converted with pymupdf4llm (AGPL, as is PyMuPDF): headings from the PDF's outline if it has one, else from its font sizes, else a section per page. A PDF without text, such as a scan, fails with a message saying so; there is no OCR.
- EPUBs are converted with ebooklib and markdownify: a chapter's headings, or its title in the table of contents.
- Two downloads: the original file, unchanged, and the material as it is now, as a PDF or an EPUB laid out anew from its Markdown (with PyMuPDF and ebooklib), with an outline of its headings or a chapter per top heading.
On a subject's page, owners choose Make lessons from a document and give a PDF, an EPUB or a Markdown file
(features/lessons.feature, features/ui_lessons.feature, backend/src/qubit_notes/domain/lessons.py). A PDF or an
EPUB is converted to Markdown as an uploaded one is, then divided by its headings at its top level, the shallowest
level with at least two of them (backend/src/qubit_notes/conversion/lessons.py), so a book whose only top heading is
its title is divided into its chapters. Each part becomes a topic named after its heading, e.g. "2 Shape functions"
(2-shape-functions), after the subject's other topics, with one Markdown material, 2-shape-functions.md, whose top
heading is the chapter's and whose sections are the headings within it; it keeps the pictures its text refers to. Text
before the first chapter is a lesson of its own, named after the book's title, or "Introduction". A topic id the
subject has already is given a number, e.g. 1-introduction-2. The lessons are made in the background
(POST /api/courses/{course}/subjects/{subject}/lessons, then GET /api/lesson-imports/{id}), and those the app
stops making are made again when it starts.
A PDF's or an EPUB's lessons keep it as their original, as a converted material does: each lesson's Download
original is the whole document as it was uploaded, e.g. fem.pdf, and Download as PDF is the lesson as it is
now, e.g. 2-shape-functions.pdf. The subject's page lists its lessons' originals under Originals, each with a
download, and a PDF can be shown there and hidden again (GET /api/courses/{course}/subjects/{subject}/originals).
The original is deleted with the last of its lessons. A Markdown document's lessons are their own text, so it isn't
kept.
My papers, a tab beside My courses (where the app opens) and in the app's menu, keeps papers in collections, e.g. "Reinforcement learning", apart from courses
(features/papers.feature, features/ui_papers.feature, backend/src/qubit_notes/domain/papers.py). A paper is
uploaded to a collection as a PDF, an EPUB, Markdown or text, and stays one paper: it is converted and read by its own
sections, but never made into lessons. Its title, authors, year, venue and DOI are filled in from it where they can be
and can be changed by the collection's owners. Notes, summaries and the assistant work on a paper as on material; the
assistant answers about the paper being read from that paper alone, and, opened on the collection, from all of its
papers. A collection is shared and deleted as a course is. See docs/papers.md.
Markdown material can have mathematics in LaTeX, typeset with KaTeX (frontend/src/components/Markdown.tsx,
features/ui_reading.feature): $…$ or \(…\) in the text, and $$…$$ or \[…\] on a line of its own, or over
several lines. As in Pandoc, $ with a space just inside it, or a digit just after the closing one, is a dollar sign,
so "$5, or $10" stays as it is; \$ is always one. LaTeX that KaTeX can't typeset is shown as written, in red. It's
typeset wherever Markdown is shown: the reader, the editor's preview, and what the AI writes.
A Markdown material refers to another part of its course with a path in double brackets, starting at the subject.
The whole guide, for students and developers, is docs/references.md; the behaviour is
specified in features/references.feature and features/ui_references.feature.
| To refer to | Write |
|---|---|
| a topic | [[numerical-methods/finite-elements]] |
| a material | [[numerical-methods/finite-elements/fem-notes.md]] |
| a section of a material | [[numerical-methods/finite-elements/fem-notes.md#2 Assembly]] |
| any of them, labelled | [[numerical-methods/finite-elements|the FEM topic]] |
- The reader shows each reference as a link: to the topic's page, to the material, or to the material at the section.
- A reference to something the course doesn't have is broken: shown struck through, not a link, and reported when
the material is saved (
broken_referencesin the answer to an upload, a write or an edit). - In the editor, Add reference picks a topic, material and section and inserts the reference; How references work shows the syntax.
- A topic's page shows the materials it is mentioned in.
- A PDF or an EPUB downloaded as it is now has each reference as its label, as plain text.
Nothing about references is stored: they are read from the materials when asked for
(backend/src/qubit_notes/domain/references.py), so they are never out of date, at the cost of a topic's page
reading every Markdown material of the course to find the mentions.
Each topic's material is in the order its owners set (features/reading_order.feature), with ↑ ↓ on the topic's
page; new material goes last. The reader links to the previous and the next topic of the subject, by name, in the
order of its topics; each opens at its first material that is ready, and topics with none are passed over. The order
is kept in the app's database (material_order, backend/src/qubit_notes/domain/material_order.py).
A course is organised into subjects, each with its topics. The course page lists its subjects by name; each opens a
page of its own with the subject's topics, their statuses, and the owners' actions on them, and each topic opens a
page with its material (features/ui_topics.feature). Owners create subjects and topics before they have material
for them: "New subject" and "New topic" on the course page, "Add topic" on a subject's page, and the Add material
page, where only the subject is needed (features/subjects.feature, features/courses.feature,
features/ui_courses.feature). Uploading to a new location creates its subject and topic too. A topic stays when its
material is deleted, and a subject when its topics are; owners delete a subject once it has no topics. Subjects are
kept in the app's database (subjects), as topics are (topics).
A subject or topic is known by its id, lowercase words joined by hyphens (e.g. finite-elements), which is in its
links and in references to it and never changes; the app shows it by its name, as typed (e.g. "Finite Elements")
(features/topic_names.feature). One created without a name, e.g. by uploading to a new location, or before names
were kept, is named after its id ("Finite elements"). Owners rename subjects on the course page or their own, and
topics on their subject's page or their own. Names are kept in the app's database (topics.name, subject_names).
A course likewise keeps its name (e.g. computer-science) in its links and is shown by its title, as typed when it was
created (features/course_names.feature); one without a title is titled after its name. Only its creator changes its
title, from My courses or the course's page; it is kept in course_titles. Owners rename a topic from its page too.
On a topic's page, owners choose Generate material (features/generating.feature,
backend/src/qubit_notes/domain/generating.py), tick the topic's materials to generate from in a menu (or All), and
ask for revision notes, a cheat sheet, combined notes or worked examples, their own instructions, or both. The AI
writes Markdown from the ticked materials alone, citing the sections it drew on by labels the app turns into
references (see References between topics), so they're links and the sources mention the
new material. It opens in the editor, unsaved, to be edited and saved in any topic.
At most 60,000 characters of material are generated from at once.
Students ask the assistant for flashcards, e.g. "Make 10 flashcards of this section" (features/flashcards.feature,
backend/src/qubit_notes/domain/flashcards.py). The AI writes a deck from the section, the document or the topic, each
card with a front, a back and the section it is from; decks are saved and marked out of date as quizzes are.
Every card is due for review as soon as its deck is written, and Reviews, in the app's menu, shows the cards due in
all the student's courses one at a time (features/reviews.feature, features/ui_reviews.feature,
backend/src/qubit_notes/domain/reviews.py). The student shows a card's answer and says how well they remembered it,
Again, Hard, Good or Easy, which schedules it as SM-2 does: Again brings it back in 10 minutes and starts it over, Good
after a day, then 6 days, then its interval times its ease (2.5 to start with), so cards the student knows come back
less and less often. A new deck of the same scope takes the place of the earlier ones in the reviews, and a reader
removed from a course no longer reviews its cards, until it is shared with them again. Each card's
schedule is kept in the app's database (flashcards, with the decks in flashcard_decks).
By default (QUBIT_NOTES_PLATFORM=standalone) the app is its own platform (features/standalone.feature):
- Material. Uploads are kept in a directory (
QUBIT_NOTES_FILES_DIR,backend/materialby default) or an S3 bucket (QUBIT_NOTES_FILES_BUCKET). In the background, each document is read into its sections (a Markdown file by its headings, a PDF by its outline or else page by page), divided into chunks and embedded. Courses, documents, sections and chunks are kept in the app's database, theplatform_*tables. - Search. On Postgres, with pgvector, which the migrations turn on (Amazon RDS has it; locally, use the
pgvector/pgvector:pg16image). On SQLite, in Python. - Embedding models (
QUBIT_NOTES_EMBEDDING_MODEL):bge-small-en-v1.5(the default) andall-minilm-l6-v2run in the app and need no key;voyage-*models, e.g.voyage-3.5-lite, needQUBIT_NOTES_VOYAGE_API_KEY;text-embedding-3-*models needQUBIT_NOTES_OPENAI_API_KEY. Material embedded with one model is found only by searching with the same model: after changing it, runqubit-notes reembed. - Memory. The in-app models embed 32 chunks at a time, so a document needs about 1.5 GB however long it is
(with fastembed's own batch of 256, a book's long lesson took 5.7 GB).
QUBIT_NOTES_INGESTION_WORKERSdocuments (2 by default) are embedded at once, each with its own share, so allow about 1.5 GB per worker beside the app's own; on a small machine, set it to 1. Hosted models embed elsewhere and need little. - The AI (
QUBIT_NOTES_LLM_PROVIDER,QUBIT_NOTES_LLM_MODEL): Anthropic's models withQUBIT_NOTES_ANTHROPIC_API_KEY(the default), or OpenAI's withQUBIT_NOTES_OPENAI_API_KEY. Without a model set, each provider uses its own:claude-sonnet-5-5, orgpt-5, soQUBIT_NOTES_LLM_PROVIDER=openaiand the OpenAI key are all OpenAI needs. The admins can choose another provider and model from the app (see below). Conversations and what the assistant remembers are kept in the app's database.
export QUBIT_NOTES_ANTHROPIC_API_KEY=sk-ant-...
make dev-backend/api/health reports the app's database, files, embeddings and AI, and reports it degraded without the key of its
model provider. Unfinished ingestions are resumed when the app starts again, so run one replica of the app.
A key the settings don't give the app, such as its Anthropic key, its admins can give it from the app
(features/app_keys.feature, features/ui_navigation.feature): the platform's status, next to the app's name, says
"Platform degraded" and opens to show what is missing, and sends an admin to Settings, whose Keys list each
provider with whether the app has its key: "Saved", "No key" or "Given by the app's settings". An admin gives a missing
key there and is told it is saved; a saved key can be replaced, or removed, after which the app has none for that
provider (DELETE /api/keys/{provider}). GET /api/keys tells whether the app has each key, never the key. The admins
are named in QUBIT_NOTES_ADMINS (e.g. alex,maria, as they sign in); anyone else is told to ask one.
The app keeps a key it is given in its database (app_keys), encrypted with QUBIT_NOTES_SECRET_KEY, and gives it to
its platform again when it starts; without the secret it keeps none. A key is never shown back, and a key in the
settings is used rather than one kept, and isn't removed from the app. Changing the secret makes the keys kept
unreadable: give them again. Where the platform is gogi, the key is stored in gogi as the app's credential instead, as
qubit-notes store-provider-key does; gogi has no way to remove one, so it can only be replaced.
A missing key is described in plain words, e.g. "The app is missing its Anthropic key.": the health check lists missing
keys by their provider (missing_keys), and never names the setting that holds one.
The admins choose the AI's provider and model on the app's Settings page, from the menu (features/ai_model.feature,
features/ui_settings.feature): Anthropic or OpenAI, and any of its models by name, with some suggested. Choosing a
provider fills in its own model, and if the app has no key for that provider, the page says so. The choice
is kept in the app's database (app_settings) and used for everyone, and again after a restart, rather than
QUBIT_NOTES_LLM_PROVIDER and QUBIT_NOTES_LLM_MODEL; where the platform is gogi, it is given to gogi with each
request. Anyone else sees the provider and model in use. The embedding model isn't chosen there: changing it means
embedding the material again.
Where the app has the keys of both Anthropic and OpenAI, the assistant, and Generate, offer each student a choice of
which answers them: the app's provider with the model chosen, or the other with its own model, e.g. "OpenAI · gpt-5".
The choice is kept in the student's browser and sent with each request (X-AI-Provider); the app answers with the
app's provider where it hasn't the key of the one chosen (ChosenProvider in backend/src/qubit_notes/api/ai_model.py).
Set QUBIT_NOTES_PLATFORM=gogi, and install gogi-python beside the app (make install-gogi, or build the image
with it, below).
Where gogi serves many applications, gogi's operator registers the app (gogi-admin register qubit-notes) and gives
it a key. Run the app with it, and store the app's Anthropic key in gogi once, as the app's own:
export QUBIT_NOTES_GATEWAY_URL=gogi.example.org:443
export QUBIT_NOTES_GOGI_API_KEY=gogi_...
export QUBIT_NOTES_GOGI_CA_FILE=/etc/qubit-notes/gogi-ca.pem # only where gogi has a certificate authority of its own
echo "$ANTHROPIC_API_KEY" | uv run qubit-notes store-provider-key anthropic/api/health reports the app as degraded if gogi does not take its key. Against a gogi without keys, as in
development, none of this is needed. The scenarios of features/gogi_application.feature run through gogi's
gateway that requires keys (localhost:50050), registering applications with docker exec gogi-gateway-applications /root/gogi-admin.
Where the app is deployed, students sign in with Amazon Cognito rather than by name
(features/sign_in.feature, features/ui_sign_in.feature). The user pool needs an app client that is public (no
client secret), with the authorization code grant, the openid scope, and the app's address, e.g.
https://notes.example.org/, as both its callback and its sign-out URL; and a domain for its sign-in pages.
export QUBIT_NOTES_AUTH=cognito
export QUBIT_NOTES_COGNITO_REGION=eu-west-2
export QUBIT_NOTES_COGNITO_USER_POOL_ID=eu-west-2_AbC123
export QUBIT_NOTES_COGNITO_CLIENT_ID=...
export QUBIT_NOTES_COGNITO_DOMAIN=https://qubit-notes.auth.eu-west-2.amazoncognito.comThe browser signs in with PKCE and sends the access token with every request; the backend checks it against the
pool's keys, issuer, app client and expiry, and the student is the token's username. A video's link carries the token
(?access_token=), as a <video> can't send headers. The tests stand in for Cognito with
backend/tests/fakes/fake_cognito.py.
One image serves the API and the built frontend on port 8000, on the app's own platform, with its default embedding model in it:
docker build -t qubit-notes .
docker run -p 8000:8000 -e QUBIT_NOTES_DATABASE_URL=postgresql+psycopg://... -e QUBIT_NOTES_FILES_BUCKET=... \
-e QUBIT_NOTES_ANTHROPIC_API_KEY=... qubit-notesWithout a bucket, uploads are kept in /app/material, which wants a volume. To run the image on gogi, build it with
gogi-python, which comes in as a build context of its own:
docker build --build-arg BACKEND=backend-gogi --build-context gogi-python=../gogi-python -t qubit-notes .
It runs as a non-root user, migrates its database when it starts, and is healthy once /api/config answers. CI builds
it for every PR and pushes ghcr.io/pockerman/qubit-notes:main from main; a release pushes :<version> and :latest.
deploy/aws is the Terraform for the app on AWS, on its own platform, or, with platform = "gogi", as
one of the applications of a gogi deployment (gogi's docs/deployment.md):
| Address | CloudFront, at https://<id>.cloudfront.net while there's no domain; it reaches the server privately, through a VPC origin |
| Server | one EC2 instance (t3.small), Amazon Linux 2023, running the image as qubit-notes.service; reached with SSM Session Manager, not SSH |
| Sign-in | a Cognito user pool, with its own sign-in pages at an amazoncognito.com address |
| Data | RDS for PostgreSQL 16 (db.t4g.micro) with pgvector, in private subnets, backed up for a week |
| Files | an S3 bucket of the students' uploads, private, reached through a gateway endpoint; replaced or deleted uploads are kept 30 days |
| Secrets | Secrets Manager: the database's password, and the app's keys of the model providers |
This costs about $40 a month in eu-west-2 at on-demand prices. The server keeps nothing of its own, so it can be replaced at any time.
-
Keep the state somewhere private, as it holds the database's password: copy
backend.tf.exampletobackend.tfand name an S3 bucket of your own. Then apply:cd deploy/aws terraform init terraform apply -
Put the app's keys of the model providers where its server reads them: Anthropic's, and Voyage AI's or OpenAI's if the embedding model is one of theirs (
app_settings = { QUBIT_NOTES_EMBEDDING_MODEL = "voyage-3.5-lite" }). If the image is private, put a token that can read it inqubit-notes/registrytoo, as{"username": "...", "token": "..."}.read -rs ANTHROPIC_API_KEY aws secretsmanager put-secret-value --secret-id "$(terraform output -raw provider_keys_secret)" \ --secret-string "$(jq -n --arg key "$ANTHROPIC_API_KEY" '{anthropic: $key}')"
-
Restart the app on its server, so it reads them:
aws ssm start-session --target "$(terraform output -raw instance_id)" sudo systemctl restart qubit-notes -
Make the students' accounts; Cognito emails each their temporary password:
aws cognito-idp admin-create-user --user-pool-id "$(terraform output -raw user_pool_id)" \ --username maria --user-attributes Name=email,Value=maria@example.org Name=email_verified,Value=true
Then open terraform output -raw url. To run another image, set image_tag, apply, and restart the app; the app's
other settings go in app_settings, e.g. { QUBIT_NOTES_LLM_MODEL = "..." }. After changing the embedding model,
embed the material again on the server: sudo docker exec qubit-notes qubit-notes reembed.
With platform = "gogi" the app keeps no files or keys of its own, and uses an image built with gogi-python (above):
-
Deploy gogi first, and give the app gogi's address and certificate authority, e.g. in
terraform.tfvars:platform = "gogi" gogi_gateway_url = "203.0.113.9:443" # gogi's gateway_address output gogi_ca_certificate = file("gogi-ca.pem") # gogi's ca_certificate output
-
Let the app's server into gogi: add
terraform output -raw server_public_ip, as<ip>/32, to gogi'sclient_cidrs, and apply gogi. -
Register the app with gogi (
gogi-admin register qubit-noteson gogi's server), and put its key interraform output -raw gogi_api_key_secret. -
On the app's server, restart the app and store its Anthropic key in gogi:
sudo systemctl restart qubit-notes read -rs ANTHROPIC_API_KEY && echo "$ANTHROPIC_API_KEY" | sudo docker exec -i qubit-notes qubit-notes store-provider-key anthropic
Pull requests are squash-merged, and the squash commit takes the PR's title as its message. So the title must be a
conventional commit: feat: delete a course,
fix(reader): keep the section on reload, docs: …, ci: …. The PR title check enforces it. Commits inside a PR
can be anything, since they are squashed away.
- CI (
.github/workflows/ci.yml) runs on every PR and onmain: ruff, ESLint and TypeScript, then every scenario on the in-memory platform, the@uiones in Chromium, and the@platformand@standaloneones on the app's own platform, with Postgres and pgvector. It needs nothing but this repository. Runs on gogi need a gogi stack, so they run locally only (make bdd-gogi). - Releases (
.github/workflows/release.yml): release-please keeps a release PR open with the next version andCHANGELOG.md, from thefeatandfixcommits onmain. Merging it tagsvX.Y.Zand publishes a GitHub release. The version is kept inbackend/pyproject.tomlandfrontend/package.json. Before 1.0, afeatbumps the minor version, as does a breaking change (feat!: …).