DEVELOPMENT.md — Betat Community Framework

Local development setup guide for contributors and operators testing before deployment. This is distinct from DISTRIBUTION.md (which covers shipping) and start-a-community.md (which covers operator installation).

Read framework/CLAUDE.md and framework/BLUEPRINT.md §0 before starting a build session. This file covers environment setup only.


Prerequisites

  • Python 3.11 installed via your OS package manager (not compiled from source)
  • Git configured for the Sylva-AF account (SSH key id_ed25519_sylva)
  • The betat-sandbox Docker container built (docker build -t betat-sandbox .)
  • A terminal inside the container with --network host for PostgreSQL access

Verify Python 3.11 is the right build before creating a virtualenv:

python3.11 --version
# Must show Python 3.11.x

python3.11 -c "import sqlite3; print('sqlite ok')"
# Must print: sqlite ok
# If this fails, your Python was compiled from source without SQLite.
# Install via package manager: dnf install python3.11  (Rocky/RHEL)
#                              apt install python3.11  (Ubuntu/Debian)
#                              brew install python@3.11 (macOS)

Step 1 — Start the sandbox container

# From the host machine:
docker run -it --rm \
    --name betat-build \
    -v ~/betat-repo:/workspace \
    -w /workspace/framework \
    --network host \
    --cap-drop ALL --cap-add SETUID --cap-add SETGID --cap-add DAC_OVERRIDE \
    betat-sandbox \
    bash

--network host is required for PostgreSQL access during dual-DB testing. --cap-add DAC_OVERRIDE is required too: the bind-mounted repo is owned by your host user (not root), and without this capability root inside the container cannot write to it at all — every pip install, .env write, betat init, even touch, fails with Permission denied despite the container prompt showing [root@...]. This is not optional.

All following commands run inside the container unless noted otherwise.


Step 2 — Fresh virtualenv

If a .venv already exists from a previous session, remove it first. A venv is tied to the Python that built it — never reuse one across Python versions or across host/container boundaries. This step must run after Step 1, inside the container. Building the venv on the host and then entering the container (or vice versa) leaves .venv/bin/python symlinked to a binary path that doesn’t exist on the other side, and every command reports python: command not found even though the venv appears active.

# Inside the container, at /workspace:
cd /workspace

# Remove old venv if present
deactivate 2>/dev/null; rm -rf .venv

# Create fresh venv with Python 3.11
python3.11 -m venv .venv

# Activate it — do this every time you open a new shell
source .venv/bin/activate
# Prompt shows (.venv) — confirms activation

# Verify
python --version              # must show Python 3.11.x
python -c "import sqlite3; print('sqlite ok')"   # must print sqlite ok
pip install --upgrade pip

Every new shell into the container needs re-activation:

source /workspace/.venv/bin/activate

Which side am I on? When unsure whether you are inside the container or on the host:

ls /.dockerenv && echo "INSIDE container" || echo "ON host"

Container prompt looks like: [root@5a01f44af990 framework] Host prompt looks like: [sylva@www betat-repo]


Step 3 — Install the framework

Run this from /workspace (the repo root), not /workspace/framework — even though Step 1’s container starts you in framework/. The install path ./framework[dev] is relative to the repo root; running it from inside framework/ points at a nonexistent nested framework/framework and fails.

cd /workspace
pip install -e "./framework[dev]"
# Installs Django, DRF, pytest, pytest-django, and all dependencies.
# The -e flag installs in editable mode — code changes take effect
# immediately without reinstalling.
# Django equivalent: pip install django djangorestframework pytest

Confirm the install:

python -c "import django; print('django', django.get_version())"
betat --help
# Should list: init, runserver, check, announce, export, start, backup

Step 4 — DNS override for development

The community id DNS check (socket.getaddrinfo) requires a real resolving domain when run interactively. Two approaches for development:

Approach A — /etc/hosts override (for the interactive prompts or the browser wizard)

Run this on the host machine (not inside the container). The container uses --network host so host DNS is visible inside:

# On the HOST, in a separate terminal:
sudo sh -c 'echo "127.0.0.1 betat-dev.local" >> /etc/hosts'
sudo sh -c 'echo "127.0.0.1 science.betat-dev.local" >> /etc/hosts'
sudo sh -c 'echo "127.0.0.1 archive.betat-dev.local" >> /etc/hosts'

# Verify from inside the container:
python3 -c "import socket; socket.getaddrinfo('betat-dev.local', None); print('resolves ok')"

Use betat-dev.local as your community id during betat init and the setup wizard. The subdomains let you test multiple community ids in the same dev session.

Approach B — non-interactive flags (fastest path, no /etc/hosts needed)

There is no BETAT_SKIP_DNS_CHECK env var — init.py doesn’t implement one. Instead, pass --id (and the other required fields) directly to betat init rather than answering the interactive prompts: the non-interactive path only warns on a non-resolving domain, it doesn’t block. See Step 7 for the full command. Any string works as --id, resolving or not.

Use Approach A when you want to walk through the interactive prompts or the browser setup wizard as an operator would. Use Approach B to get to a running server fastest.


Step 5 — Create the .env file

betat init writes .env during setup (BETAT_SECRET_KEY, BETAT_OPERATOR_EMAIL, etc. — see framework/.env.example for the full reference), but for development create it first with the dev-relevant values. All variables settings.py reads are BETAT_-prefixed — SECRET_KEY/DEBUG/ALLOWED_HOSTS (no prefix) are not read by anything and silently do nothing.

Use printf, not a heredoc — pasting a multi-line cat > .env << 'EOF' block into some terminals auto-indents the pasted lines, which shifts the closing EOF off column 0 and leaves the shell hung at a > prompt waiting for a delimiter that will never match. Ctrl-C breaks out of that if it happens.

# From /workspace/framework:
printf '%s\n' \
  'BETAT_DEBUG=true' \
  'BETAT_ALLOWED_HOSTS=localhost,127.0.0.1,betat-dev.local' > .env

betat init fills in BETAT_SECRET_KEY and the accountability fields on top of this — it never overwrites a key that’s already present.


Step 6 — Migrate and create a superuser

This must run before Step 7 (betat init). init checks CommunityConfig.objects.exists() as its first step, which queries a table that doesn’t exist until migrations have run — running init first fails with no such table: core_communityconfig.

python manage.py migrate
# Creates all database tables.
# Run this after every pull that includes new migrations.
# Django equivalent: python manage.py migrate

python manage.py createsuperuser
# Creates the first verifier account.
# Username and password are yours to choose for dev.
# Django equivalent: python manage.py createsuperuser

Step 7 — Run betat init

Guided prompts (pairs with Step 4’s Approach A):

cd /workspace/framework
betat init
# Complete the operator declaration and email steps — these are real,
# the same accountability record ships in production.

Or non-interactive flags (Step 4’s Approach B — faster, no /etc/hosts needed; still prompts for the operator declaration and email, those aren’t skippable by design):

betat init --id test.betat-dev.local --name "Test Community" \
  --domain "general testing" --content-type text \
  --store-uri "http://localhost:8000/betat" \
  --auth-method community_peer_vouching
# --content-type must be one of CONTENT_TYPE_CHOICES (core/models.py).
# --auth-method must be one of PROTOCOL_LIST (communityauth/floor.py) —
# community_peer_vouching, cryptographic_signature, institutional_endorsement.

Alternatively, run betat start first and use the browser wizard at http://localhost:8000/community/install/.


Step 8 — Verify and start

python manage.py check
# Checks Django configuration for errors.
# In dev (BETAT_DEBUG=true), production warnings are expected and acceptable.
# Django equivalent: python manage.py check
# Production check: python manage.py check --deploy

betat start
# Starts the development server at http://localhost:8000
# Django equivalent: python manage.py runserver 0.0.0.0:8000
# Production server: gunicorn betat_community.wsgi:application

Visit http://localhost:8000/community/ in your browser.

What you should see:

  • If CommunityConfig does not exist yet: the Phase 1 installer screen with the eclipse animation
  • If CommunityConfig exists: the community UI (Records, Enroll, Submit, Review queue) with your community name in the nav

Running tests

# From /workspace/framework, venv active:
pytest tests/ -v
# Runs the full test suite.
# Django equivalent: python manage.py test (pytest-django wraps this)

# Run a specific test file:
pytest tests/test_store.py -v

# Run with PostgreSQL (dual-DB ship gate):
BETAT_DB=postgresql://postgres@localhost:5432/betat pytest tests/ -v
# Requires PostgreSQL running and the betat database created.
# The developer runs this before shipping — not Claude Code.

Common development tasks

Reset the community config and start over:

python manage.py shell -c "
from betat_community.core.models import CommunityConfig
CommunityConfig.objects.all().delete()
print('CommunityConfig cleared')
"
# Then run betat init again.

Check what is in the database:

python manage.py shell
# Inside the shell:
from betat_community.core.models import CommunityConfig
from betat_community.store.models import ProvenanceRecord
print(CommunityConfig.objects.first())
print(ProvenanceRecord.objects.count())
exit()
# Django equivalent: python manage.py shell (same command)
# DB shell: python manage.py dbshell

Back up the dev database:

betat backup
# SQLite: copies betat.sqlite3 to a timestamped backup file.
# Django equivalent: cp betat.sqlite3 betat_backup_$(date +%Y%m%d).sqlite3

Apply new migrations after a pull:

git pull                      # developer action
source /workspace/.venv/bin/activate
pip install -e "./framework[dev]"   # in case dependencies changed
python manage.py migrate

Check SSH auth before pushing:

ssh-add ~/.ssh/id_ed25519_sylva     # load the Sylva-AF key
ssh -T git@github.com               # confirm: Hi Sylva-AF!

Dual-DB testing (before shipping)

Per BLUEPRINT §0 dual-database ship promise, the store and acceptance test suites must pass on both SQLite and PostgreSQL before v0.1 ships. The developer runs this — Claude Code does not.

# 1. Confirm PostgreSQL is accessible (--network host required):
python -c "
import psycopg
psycopg.connect('postgresql://postgres@localhost:5432/postgres',
                connect_timeout=5).close()
print('postgres reachable')
"

# 2. Create the betat database if it does not exist:
# Run on the HOST:
psql -U postgres -c "CREATE DATABASE betat;"

# 3. Run the suite against SQLite (default):
pytest tests/ -v

# 4. Run the suite against PostgreSQL:
BETAT_DB=postgresql://postgres@localhost:5432/betat \
python manage.py migrate && \
BETAT_DB=postgresql://postgres@localhost:5432/betat \
pytest tests/ -v

# Both must pass before shipping.

Troubleshooting

sqlite3 import fails: Your Python 3.11 was compiled from source without SQLite. Install via OS package manager (see Prerequisites above).

pip: command not found after entering the container: The venv is not active. Run source /workspace/.venv/bin/activate.

python: command not found even though the venv shows (.venv) active: The venv was built on the wrong side of the host/container boundary (see Step 2) — its bin/python symlink points at a binary path that doesn’t exist here. Delete it and rebuild inside the container: rm -rf .venv && python3.11 -m venv .venv && source .venv/bin/activate.

Permission denied writing anywhere under /workspace, even as root: The container is missing --cap-add DAC_OVERRIDE (see Step 1). The bind-mounted repo is owned by your host user, and without that capability root inside the container can’t bypass ordinary file-permission checks — it’s just another non-owning uid as far as the write check is concerned.

A pasted cat > .env << 'EOF' ... EOF block hangs at a > prompt: The terminal indented the pasted lines, so the closing EOF isn’t flush at column 0 and bash never matches it. Ctrl-C to break out, then use the printf form in Step 5 instead of a heredoc.

betat-dev.local does not resolve inside the container: The container must be launched with --network host. Check: ls /.dockerenv confirms you are inside the container.

django.db.utils.OperationalError: no such table: core_communityconfig from betat init: python manage.py migrate hasn’t run yet. Step 6 must come before Step 7 — init checks for an existing CommunityConfig as its first action.

git push asks for a password: The SSH agent does not have the Sylva-AF key loaded. Run ssh-add ~/.ssh/id_ed25519_sylva then retry.

CommunityConfig.objects.exists() returns True but you want a clean start: Use the reset command above (manage.py shell -c delete). The middleware redirects to the installer only when no config exists.

manage.py check --deploy warns about DEBUG=True: Expected in development when BETAT_DEBUG=true is in .env. Acceptable for dev — resolve before going public.


File reference

File Purpose
framework/BLUEPRINT.md Authoritative build decisions — read first
framework/CLAUDE.md Session bootstrap for Claude Code
framework/TODO.md Build plan and section status
framework/todos/ Per-section task checklists
framework/DISTRIBUTION.md How to ship and publish the package
framework-production.md Recommended production stack, PostgreSQL role setup
ROADMAP.md Where the project goes after v0.1
DEVELOPMENT.md This file — local dev setup
.env Local environment variables (gitignored)
framework/.env.example Template for .env (committed, no secrets)

Betat. The human record, for the other person.


Comments & ideas

This site uses Just the Docs, a documentation theme for Jekyll.