Skip to Content
DocsGetting StartedFirst Project

Your First Django-CFG Project

What You’ll Build A complete multi-tenant SaaS application with type-safe YAML configuration, workspace management, and production-ready setup in under 15 minutes.

Project Creation Flow

Goal

Create a Django-based SaaS platform with type-safe YAML configuration using Django-CFG.

Prerequisites

Requirements Before starting, ensure you have:

  • ✅ Python 3.12+ installed (Installation Guide)
  • ✅ Basic Django knowledge
  • ✅ Code editor with Python support (VS Code, PyCharm, etc.)
  • ✅ Command line/terminal access

Step-by-Step Tutorial

1. Create Project Structure

Linux/macOS

# Create project directory mkdir saas-platform cd saas-platform # Create virtual environment python3.12 -m venv .venv source .venv/bin/activate # Install Django and Django-CFG pip install django django-cfg # Create Django project django-admin startproject core .

Windows

# Create project directory mkdir saas-platform cd saas-platform # Create virtual environment python -m venv .venv .venv\Scripts\activate # Install Django and Django-CFG pip install django django-cfg # Create Django project django-admin startproject core .

Windows PowerShell If you get an execution policy error, run:

Set-ExecutionPolicy -ExecutionPolicy RemoteSigned -Scope CurrentUser

Verify Installation Check that Django-CFG is installed correctly:

python -c "import django_cfg; print(django_cfg.__version__)"

2. Create Environment Configuration Module

Create directory structure:

saas-platform/ ├── core/ │ ├── __init__.py │ ├── environment/ # New directory │ │ ├── __init__.py # New file │ │ ├── loader.py # New file │ │ └── config.dev.yaml # New file │ ├── config.py # New file │ ├── settings.py │ ├── urls.py │ └── wsgi.py └── manage.py

core/environment/init.py:

"""Environment configuration loader.""" from .loader import env __all__ = ["env"]

core/environment/loader.py:

""" Environment configuration with [Pydantic models](../../fundamentals/core/type-safety) and YAML loading. Based on django-cfg libs/django_cfg_opensource/sample/django/api/environment/loader.py """ import os from pathlib import Path from pydantic import BaseModel, Field, computed_field from pydantic_yaml import parse_yaml_file_as # Environment detection IS_DEV = os.environ.get("IS_DEV", "").lower() in ("true", "1", "yes") IS_PROD = os.environ.get("IS_PROD", "").lower() in ("true", "1", "yes") IS_TEST = os.environ.get("IS_TEST", "").lower() in ("true", "1", "yes") # Default to development if not any([IS_DEV, IS_PROD, IS_TEST]): IS_DEV = True class DatabaseConfig(BaseSettings): """Database configuration.""" url: str = Field(default="sqlite:///db.sqlite3") model_config = SettingsConfigDict( env_prefix="DATABASE__", env_nested_delimiter="__", ) class AppConfig(BaseSettings): """Application configuration.""" name: str = Field(default="SaaS Platform") site_url: str = Field(default="http://localhost:3000") api_url: str = Field(default="http://localhost:8000") model_config = SettingsConfigDict( env_prefix="APP__", env_nested_delimiter="__", ) class EnvironmentMode(BaseSettings): """Environment mode detection.""" is_test: bool = Field(default=False) is_dev: bool = Field(default=False) is_prod: bool = Field(default=False) @model_validator(mode="after") def set_default_env(self): """Set development as default if no env specified.""" if not any([self.is_test, self.is_dev, self.is_prod]): self.is_dev = True return self @computed_field @property def env_mode(self) -> str: """Get environment mode string.""" if self.is_test: return "test" elif self.is_dev: return "development" elif self.is_prod: return "production" return "development" class EnvironmentConfig(BaseSettings): """Complete environment configuration.""" # Core settings secret_key: str = Field( default="dev-secret-key-at-least-fifty-characters-long-for-django-security" ) debug: bool = Field(default=True) # Configuration sections database: DatabaseConfig = Field(default_factory=DatabaseConfig) app: AppConfig = Field(default_factory=AppConfig) env: EnvironmentMode = Field(default_factory=EnvironmentMode) # Security security_domains: list[str] | None = None model_config = SettingsConfigDict( env_file=str(Path(__file__).parent / ".env"), env_file_encoding="utf-8", env_nested_delimiter="__", case_sensitive=False, extra="ignore", ) # Global environment configuration instance # Auto-loads from ENV > .env > defaults env = EnvironmentConfig()

core/environment/.env:

# Development configuration SECRET_KEY="dev-secret-key-at-least-fifty-characters-long-for-django-security" DEBUG=true # Application APP__NAME="SaaS Platform" APP__SITE_URL="http://localhost:3000" APP__API_URL="http://localhost:8000" # Database DATABASE__URL="sqlite:///db.sqlite3" # Security domains - optional in development # Django-CFG auto-configures for dev convenience: # - CORS fully open (CORS_ALLOW_ALL_ORIGINS=True) # - Docker IPs work automatically # - localhost any port # SECURITY_DOMAINS="localhost,127.0.0.1"

3. Create Configuration Class

core/config.py:

""" SaaS Platform configuration using Django-CFG. """ from django_cfg import DjangoConfig, DatabaseConfig from typing import Dict from .environment import env # Type-safe ENV loader class SaaSConfig(DjangoConfig): """SaaS platform configuration from environment variables.""" # From environment secret_key: str = env.secret_key debug: bool = env.debug env_mode: str = env.env.env_mode # Project info project_name: str = env.app.name site_url: str = env.app.site_url api_url: str = env.app.api_url # Security (optional in development, required in production) security_domains: list[str] | None = env.security_domains # Database from YAML URL (see /fundamentals/database for multi-database setup) databases: Dict[str, DatabaseConfig] = { "default": DatabaseConfig.from_url(url=env.database.url) } # Project apps project_apps: list[str] = [ "core.apps.workspaces", # Will create this app ] # Create config instance config = SaaSConfig()

4. Update settings.py

Replace core/settings.py content:

""" Django settings for SaaS platform. Uses Django-CFG for type-safe configuration. """ from pathlib import Path from .config import config # Build paths BASE_DIR = Path(__file__).resolve().parent.parent # Import all Django-CFG settings globals().update(config.get_all_settings()) # Override BASE_DIR (Django-CFG doesn't manage this) BASE_DIR = BASE_DIR # Static files STATIC_URL = '/static/' STATIC_ROOT = BASE_DIR / 'staticfiles' MEDIA_URL = '/media/' MEDIA_ROOT = BASE_DIR / 'media' # Default primary key DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'

5. Create Workspace App

# Create workspaces app python manage.py startapp workspaces core/apps/workspaces # Create __init__.py in apps directory mkdir -p core/apps touch core/apps/__init__.py

core/apps/workspaces/models.py:

from django.db import models from django.contrib.auth.models import User class Workspace(models.Model): """Multi-tenant workspace for SaaS platform.""" name = models.CharField(max_length=200) slug = models.SlugField(unique=True) description = models.TextField(blank=True) owner = models.ForeignKey(User, on_delete=models.CASCADE, related_name='owned_workspaces') created_at = models.DateTimeField(auto_now_add=True) updated_at = models.DateTimeField(auto_now=True) is_active = models.BooleanField(default=True) # SaaS features plan = models.CharField(max_length=50, default='free', choices=[ ('free', 'Free'), ('pro', 'Professional'), ('enterprise', 'Enterprise') ]) max_members = models.IntegerField(default=5) class Meta: ordering = ['-created_at'] indexes = [ models.Index(fields=['slug']), models.Index(fields=['owner', 'is_active']), ] def __str__(self): return self.name class WorkspaceMember(models.Model): """Workspace membership with roles.""" workspace = models.ForeignKey(Workspace, on_delete=models.CASCADE, related_name='members') user = models.ForeignKey(User, on_delete=models.CASCADE, related_name='workspace_memberships') role = models.CharField(max_length=20, default='member', choices=[ ('owner', 'Owner'), ('admin', 'Admin'), ('member', 'Member'), ('viewer', 'Viewer'), ]) joined_at = models.DateTimeField(auto_now_add=True) class Meta: unique_together = [['workspace', 'user']] ordering = ['-joined_at'] def __str__(self): return f"{self.user.username} - {self.workspace.name} ({self.role})"

core/apps/workspaces/admin.py:

from django.contrib import admin from .models import Workspace, WorkspaceMember @admin.register(Workspace) class WorkspaceAdmin(admin.ModelAdmin): list_display = ['name', 'owner', 'plan', 'is_active', 'created_at'] list_filter = ['plan', 'is_active', 'created_at'] search_fields = ['name', 'description'] prepopulated_fields = {'slug': ('name',)} readonly_fields = ['created_at', 'updated_at'] @admin.register(WorkspaceMember) class WorkspaceMemberAdmin(admin.ModelAdmin): list_display = ['workspace', 'user', 'role', 'joined_at'] list_filter = ['role', 'joined_at'] search_fields = ['workspace__name', 'user__username'] readonly_fields = ['joined_at']

6. Run Migrations and Create Superuser

# Create database tables python manage.py migrate # Create admin user python manage.py createsuperuser # Enter: username, email, password # Run development server python manage.py runserver

7. Access Your Application

Open browser:

Login to admin with created superuser credentials.

Production Configuration

Create core/environment/config.prod.yaml:

secret_key: "" # Set via environment variable or secrets manager debug: false app: name: "SaaS Platform" site_url: "https://platform.example.com" api_url: "https://api.platform.example.com" # REQUIRED in production - Django-CFG auto-normalizes any format security_domains: - "platform.example.com" # ✅ No protocol - "api.platform.example.com" # ✅ Subdomain # SSL/TLS: Assumes reverse proxy (nginx, Cloudflare, etc.) handles HTTPS # No ssl_redirect needed - Django-CFG defaults to reverse proxy mode database: url: "postgresql://user:[email protected]:5432/saas_prod"

Deploy with:

IS_PROD=true python manage.py runserver

Adding Features

Enable Built-in Apps

Edit core/config.py:

class SaaSConfig(DjangoConfig): # ... existing config ... # Enable built-in features (see /features/built-in-apps/overview for all apps) enable_support: bool = True # Support tickets enable_accounts: bool = True # Extended user management enable_newsletter: bool = True # Email campaigns enable_payments: bool = True # Subscription billing

Add Caching

Update core/environment/loader.py (see Cache Configuration for advanced options):

class EnvironmentConfig(BaseModel): # ... existing fields ... # Cache redis_url: str | None = None

Update core/environment/config.dev.yaml:

# ... existing config ... redis_url: "redis://localhost:6379/0"

Update core/config.py:

from django_cfg import DjangoConfig, DatabaseConfig class SaaSConfig(DjangoConfig): # ... existing config ... # ✨ Auto Redis cache - just set redis_url! redis_url: str | None = env.redis_url # Django-CFG automatically creates CacheConfig if redis_url is set

Troubleshooting

See Troubleshooting Guide for complete solutions.

Configuration Not Loading

# Check which config file is loaded python manage.py shell >>> from core.environment import env >>> print(env.env.env_mode) development

YAML Parse Error

# Validate YAML syntax python -c "import yaml; yaml.safe_load(open('core/environment/config.dev.yaml'))"

Pydantic Validation Error

# Check error details python manage.py check

Next Steps

TAGS: tutorial, first-project, yaml, pydantic, django-cfg, saas, multi-tenant DEPENDS_ON: [installation, configuration] USED_BY: [configuration, built-in-apps]