Reference Implementation
The ZineCore2 reference implementation is a production-ready Django REST API server that implements all four profiles (ZineCore2, AgentCore2, HoldingCore2, RepoCore2) with full CRUD operations, content negotiation, and multiple export formats.
What It Provides
- RESTful API for all four ZineCore2 profiles
- PostgreSQL backend with optimized schema design
- JSON Schema validation on all writes
- Multiple output formats — JSON, JSON-LD, CSV, DC XML, RDF/Turtle, BibTeX, MARCXML
- Content negotiation via HTTP Accept headers
- Controlled vocabularies loaded from specification
- Authentication & permissions — Public reads, authenticated writes
- Production-ready — Battle-tested Django REST Framework
Technology Stack
| Component | Technology | Version |
|---|---|---|
| Language | Python | 3.12+ |
| Framework | Django | 6.x |
| API | Django REST Framework | Latest |
| Database | PostgreSQL | 16+ |
| Package Manager | uv | Latest |
| Deployment | Docker / systemd + nginx | - |
Architecture Highlights
1. Dual Identifier Pattern
Internal PKs + External IDs:
- Integer BigAutoField primary keys (internal, auto-increment)
- Separate CharField for external identifiers (zine_id, agent_id, repo_id, holding_id)
- Users interact with external IDs; database uses internal PKs for efficiency
Example:
class Zine(models.Model):
id = models.BigAutoField(primary_key=True) # Internal: 12345
zine_id = models.CharField(max_length=255, unique=True) # External: "zine_mutate_3_1st"
title = models.CharField(max_length=500)
# ... more fields
Why? Separates database performance concerns from API design. External IDs can be human-readable while internal PKs remain optimized integers.
2. ArrayField for Repeatable Elements
Uses PostgreSQL ArrayField for fields that can have multiple values:
class Zine(models.Model):
# ...
creator = models.ArrayField(
models.CharField(max_length=255),
help_text="Agent IDs from AgentCore2"
)
subject = models.ArrayField(
models.CharField(max_length=100),
help_text="Subject terms from subjects vocabulary"
)
Why? Avoids join tables for simple repeatable fields, improving query performance and simplifying API responses.
3. Read/Write Serializer Pattern
Separate serializers for reading and writing:
Read Serializer:
class ZineReadSerializer(serializers.ModelSerializer):
# Resolves agent_ids to full agent objects
creator = AgentReadSerializer(many=True, source='creator_agents')
Write Serializer:
class ZineWriteSerializer(serializers.ModelSerializer):
# Accepts just the IDs
creator = serializers.ListField(child=serializers.CharField())
Why? Reads return fully resolved nested objects for convenience. Writes accept simple ID references for ease of use.
4. App Structure
backend/
├── core/ # Shared utilities, base models
├── catalog/ # ZineCore2 implementation
│ ├── models.py # Zine model
│ ├── serializers.py # Read/write serializers
│ └── views.py # API endpoints
├── agents/ # AgentCore2 implementation
├── holdings/ # HoldingCore2 implementation
├── repositories/ # RepoCore2 implementation
├── geography/ # Countries, regions (ISO 3166)
├── languages/ # Language codes (ISO 639)
└── vocabularies/ # Controlled vocabularies
Each app follows the same pattern:
- models.py — Django models (one per profile)
- serializers.py — DRF serializers (read + write)
- views.py — ViewSets with CRUD operations
- urls.py — URL routing
5. Database Schema Design
Key decisions:
- One table per profile (zines, agents, holdings, repositories)
- ForeignKey relationships where appropriate (holdings → zines, holdings → repositories)
- Vocabulary tables for controlled terms with referential integrity
- ArrayFields instead of many-to-many for simple repeatability
- JSON fields for flexible metadata (when needed)
Example schema:
-- Zines table
CREATE TABLE catalog_zine (
id BIGSERIAL PRIMARY KEY,
zine_id VARCHAR(255) UNIQUE NOT NULL,
title VARCHAR(500) NOT NULL,
creator VARCHAR(255)[] NOT NULL, -- Array of agent_ids
subject VARCHAR(100)[] NOT NULL, -- Array of subject codes
genre VARCHAR(100)[] NOT NULL, -- Array of genre codes
date VARCHAR(100)[] NOT NULL,
language VARCHAR(10)[] NOT NULL,
rights VARCHAR(100)[] NOT NULL,
-- ... more fields
);
6. Content Negotiation
Supports multiple output formats via HTTP Accept headers:
| Format | Accept Header | Use Case |
|---|---|---|
| JSON (default) | application/json | API consumption, web apps |
| JSON-LD | application/ld+json | Semantic web, RDF integration |
| CSV | text/csv | Spreadsheet export, data analysis |
| DC XML | application/xml | Dublin Core XML serialization |
| RDF/Turtle | text/turtle | Triple stores, SPARQL |
| BibTeX | application/x-bibtex | Citation management |
| MARCXML | application/marcxml+xml | Library systems integration |
Example:
# JSON (default)
curl https://localhost:8000/api/zines/zine_mutate_3_1st/
# JSON-LD
curl -H "Accept: application/ld+json" \
https://localhost:8000/api/zines/zine_mutate_3_1st/
# CSV
curl -H "Accept: text/csv" \
https://localhost:8000/api/zines/
7. Authentication & Permissions
Default policy:
- Read operations — Public, no authentication required
- Write operations — Require authentication (token-based)
Token authentication:
# Get token (after creating user)
curl -X POST https://localhost:8000/api/auth/token/ \
-d "username=admin&password=yourpassword"
# Use token
curl -H "Authorization: Token abc123..." \
-X POST https://localhost:8000/api/zines/ \
-d '{"title": "My Zine", ...}'
Customizable: Can be configured for institution-specific access control.
API Endpoints
ZineCore2 (Zines)
GET /api/zines/ # List all zines
POST /api/zines/ # Create zine (auth required)
GET /api/zines/{zine_id}/ # Get specific zine
PUT /api/zines/{zine_id}/ # Update zine (auth required)
PATCH /api/zines/{zine_id}/ # Partial update (auth required)
DELETE /api/zines/{zine_id}/ # Delete zine (auth required)
AgentCore2 (Creators/Contributors)
GET /api/agents/ # List all agents
POST /api/agents/ # Create agent (auth required)
GET /api/agents/{agent_id}/ # Get specific agent
PUT /api/agents/{agent_id}/ # Update agent (auth required)
DELETE /api/agents/{agent_id}/ # Delete agent (auth required)
HoldingCore2 (Holdings)
GET /api/holdings/ # List all holdings
POST /api/holdings/ # Create holding (auth required)
GET /api/holdings/{holding_id}/ # Get specific holding
PUT /api/holdings/{holding_id}/ # Update holding (auth required)
DELETE /api/holdings/{holding_id}/ # Delete holding (auth required)
RepoCore2 (Repositories)
GET /api/repositories/ # List all repositories
POST /api/repositories/ # Create repository (auth required)
GET /api/repositories/{repo_id}/ # Get specific repository
PUT /api/repositories/{repo_id}/ # Update repository (auth required)
DELETE /api/repositories/{repo_id}/ # Delete repository (auth required)
Vocabularies
GET /api/vocabularies/ # List all vocabularies
GET /api/vocabularies/{vocab_id}/ # Get specific vocabulary
Quick Start
Prerequisites
- Python 3.12+ with uv package manager
- PostgreSQL 16+ database
- Git
Installation (Development)
# Clone repository
git clone https://github.com/ZineCore2/server.git
cd server/
# Run interactive onboarding
./onboarding.sh
# Start development server
./start.sh
# Server running at http://localhost:8000
The onboarding.sh script will:
- Check prerequisites
- Create Python virtual environment with uv
- Install dependencies
- Set up PostgreSQL database
- Run migrations
- Load vocabularies from spec
- Create superuser account
Data Management
Loading Vocabularies
Vocabularies are loaded from the spec/ submodule:
# Load all vocabularies
.venv/bin/python backend/manage.py load_vocabularies
# Load specific vocabulary
.venv/bin/python backend/manage.py load_vocabularies --vocab subjects
Source: /Users/andrew/Develop/ZineCore2/spec/vocabularies/canonical/*.json
Database Migrations
# Create migrations after model changes
.venv/bin/python backend/manage.py makemigrations
# Apply migrations
.venv/bin/python backend/manage.py migrate
# Show migration status
.venv/bin/python backend/manage.py showmigrations
Management Commands
| Command | Purpose |
|---|---|
| load_vocabularies | Load controlled vocabularies from spec |
| load_geonames | Load geographic data (countries, regions) |
| load_languages | Load ISO 639 language codes |
| load_countries | Load ISO 3166 country codes |
Development Workflow
# Activate virtual environment
source .venv/bin/activate
# Run development server
cd backend/
python manage.py runserver
# Run tests
pytest
# Check code quality
ruff check .
black --check .
# Apply formatting
black .
Deployment
Option 1: Docker
# Build image
docker build -t zinecore2-server .
# Run container
docker run -p 8000:8000 \
-e DATABASE_URL=postgresql://... \
zinecore2-server
Option 2: systemd + nginx
# Configure systemd service
sudo cp zinecore2.service /etc/systemd/system/
sudo systemctl enable zinecore2
sudo systemctl start zinecore2
# Configure nginx reverse proxy
sudo cp nginx-zinecore2.conf /etc/nginx/sites-available/
sudo ln -s /etc/nginx/sites-available/nginx-zinecore2.conf \
/etc/nginx/sites-enabled/
sudo systemctl reload nginx
Configuration
Environment Variables
# Development
DJANGO_SETTINGS_MODULE=zinecore.settings.development
# Production
DJANGO_SETTINGS_MODULE=zinecore.settings.production
DATABASE_URL=postgresql://user:pass@host/dbname
SECRET_KEY=your-secret-key-here
ALLOWED_HOSTS=zinecore.example.com
CORS_ALLOWED_ORIGINS=https://yourfrontend.com
Settings Modules
- zinecore.settings.development — Development with DEBUG=True
- zinecore.settings.production — Production with DEBUG=False
- zinecore.settings.testing — Test suite configuration
Next Steps
- Installation Guide — Detailed setup instructions
- First Steps — Create your first records
- Architecture — Deep dive into models and serializers
- API Reference — Complete endpoint documentation