Django
Django connects to DB9 through the standard PostgreSQL backend using psycopg2. No special
adapter or driver is needed — Django’s ORM and its migration framework work as expected,
including makemigrations, the built-in auth/contenttypes/sessions migrations, and
ALTER-style migrations that add a field. inspectdb works as well, with two blemishes in its
output; see Troubleshooting.
Prerequisites
Section titled “Prerequisites”- A DB9 database (create one)
- Python 3.10+
- Django 4.2+
Create a DB9 Database
Section titled “Create a DB9 Database”db9 create --name django-appdb9 create prints the connection string, with the admin password, once — copy it from the Connection String: line. It has this shape:
postgresql://YOUR_TENANT_ID.admin:YOUR_PASSWORD@pg.d0000.db9.io:5432/postgresYOUR_TENANT_ID is the tenant ID prefix in the generated connection string. Copy the username from the Connection String: line printed by db9 create or from db9 db connect. The examples below use pg.db9.io:5433, which reaches the same server; either host and port works. db9 db status django-app shows the connection string without the password; if you lose it, db9 db reset-password django-app sets a new password and prints a new connection string.
Settings that take the password on its own need it unencoded: the connection string URL-encodes it (%2B is +, %3D is =), while db9 create --show-password and db9 db reset-password also print it as plain text on the Admin Pass: line.
Set the connection string as an environment variable:
DATABASE_URL="postgresql://YOUR_TENANT_ID.admin:YOUR_PASSWORD@pg.db9.io:5433/postgres?sslmode=require"Configure Database Connection
Section titled “Configure Database Connection”Install dependencies:
pip install django psycopg2-binary dj-database-url python-dotenvimport dj_database_urlfrom dotenv import load_dotenv
load_dotenv(".env.local")
DATABASES = { "default": dj_database_url.config( default="postgresql://YOUR_TENANT_ID.admin:YOUR_PASSWORD@pg.db9.io:5433/postgres?sslmode=require", conn_max_age=600, )}Install dependencies:
pip install django psycopg2-binaryDATABASES = { "default": { "ENGINE": "django.db.backends.postgresql", "NAME": "postgres", "USER": "YOUR_TENANT_ID.admin", "PASSWORD": "YOUR_PASSWORD", "HOST": "pg.db9.io", "PORT": "5433", "OPTIONS": { "sslmode": "require", }, }}Define Models
Section titled “Define Models”from django.db import models
class User(models.Model): email = models.EmailField(unique=True) name = models.CharField(max_length=100) created_at = models.DateTimeField(auto_now_add=True)
class Meta: db_table = "users"
def __str__(self): return self.name
class Post(models.Model): title = models.CharField(max_length=500) content = models.TextField(blank=True) published = models.BooleanField(default=False) author = models.ForeignKey(User, on_delete=models.CASCADE, related_name="posts") created_at = models.DateTimeField(auto_now_add=True)
class Meta: db_table = "posts"
def __str__(self): return self.titleRun Migrations
Section titled “Run Migrations”Generate and apply migrations:
python manage.py makemigrations blogpython manage.py migrateVerify the tables exist:
python manage.py dbshell\dtViews and URLs
Section titled “Views and URLs”List view
Section titled “List view”from django.http import JsonResponsefrom .models import User, Post
def user_list(request): users = User.objects.all().values("id", "name", "email", "created_at") return JsonResponse(list(users), safe=False)
def create_user(request): if request.method == "POST": import json data = json.loads(request.body) user = User.objects.create( email=data["email"], name=data["name"], ) return JsonResponse({"id": user.id, "name": user.name}, status=201) return JsonResponse({"error": "POST required"}, status=405)
def post_list(request): posts = Post.objects.select_related("author").values( "id", "title", "content", "author__name", "created_at" ) return JsonResponse(list(posts), safe=False)URL configuration
Section titled “URL configuration”from django.urls import pathfrom . import views
urlpatterns = [ path("users/", views.user_list, name="user-list"), path("users/create/", views.create_user, name="create-user"), path("posts/", views.post_list, name="post-list"),]from django.urls import path, include
urlpatterns = [ path("api/", include("blog.urls")),]Django REST Framework
Section titled “Django REST Framework”For a full API layer, add Django REST Framework:
pip install djangorestframeworkfrom rest_framework import serializersfrom .models import User, Post
class PostSerializer(serializers.ModelSerializer): class Meta: model = Post fields = ["id", "title", "content", "published", "author", "created_at"]
class UserSerializer(serializers.ModelSerializer): posts = PostSerializer(many=True, read_only=True)
class Meta: model = User fields = ["id", "email", "name", "posts", "created_at"]from rest_framework import viewsetsfrom .models import User, Postfrom .serializers import UserSerializer, PostSerializer
class UserViewSet(viewsets.ModelViewSet): queryset = User.objects.prefetch_related("posts").all() serializer_class = UserSerializer
class PostViewSet(viewsets.ModelViewSet): queryset = Post.objects.select_related("author").all() serializer_class = PostSerializerfrom django.urls import path, includefrom rest_framework.routers import DefaultRouterfrom .views_api import UserViewSet, PostViewSet
router = DefaultRouter()router.register("users", UserViewSet)router.register("posts", PostViewSet)
urlpatterns = [ path("api/", include(router.urls)),]Production Notes
Section titled “Production Notes”- Server-side only: Django runs on the server by default, so all DB queries go through the backend. Never log or render the connection string.
- Connection pooling: Set
CONN_MAX_AGEto reuse connections across requests. For high-traffic apps, considerdjango-db-connection-poolor PgBouncer. - Host and port:
pg.db9.io:5433and thepg.d0000.db9.io:5432thatdb9 createprints reach the same server; port5432works on either host. - TLS: Always include
sslmode=requirein the connection string or set it inOPTIONSfor DB9’s hosted service. - WSGI server: Use Gunicorn or uWSGI in production. Django’s development server (
runserver) is not designed for production traffic.
Troubleshooting
Section titled “Troubleshooting”Connection refused on port 5432
Section titled “Connection refused on port 5432”DB9 accepts port 5432 as well as 5433, so the client is not reaching DB9 at all. A refusal from localhost or 127.0.0.1 means Django did not get your DB9 settings — check that DATABASE_URL is loaded, or that HOST is set in the direct config.
OperationalError: FATAL: authentication failed
Section titled “OperationalError: FATAL: authentication failed”Verify the username format. DB9 expects <tenant_id>.admin. Use the complete username from the generated connection string or db9 db connect; django-app.admin fails even with the right password.
Migrations fail with relation already exists
Section titled “Migrations fail with relation already exists”If you created tables manually before running migrations, use python manage.py migrate --fake to mark existing migrations as applied. Then run future migrations normally.
inspectdb output
Section titled “inspectdb output”manage.py inspectdb reflects a DB9 database and writes a model for each of your tables —
columns, primary keys, unique=True and ForeignKey relations included. Two things in the
output need a manual pass:
-
Three stub comments for
pg_cron’s tables. Every DB9 database hasjob,job_run_detailsandrunning_jobsin thecronschema.inspectdblists them next to your own tables and then cannot read them:# Unable to inspect table 'job'# The error was: relation "job" does not existThese are comments — the command still exits
0and the rest of the file is valid Python. Delete them, or name the tables you want:python manage.py inspectdb users posts. -
CharFieldcomes back withoutmax_length. AVARCHAR(100)column is generated asmodels.CharField(). Add the lengths by hand;information_schema.columnsreports them incharacter_maximum_length.
The stubs have the same cause as Alembic’s stray tables: Django lists tables with
pg_table_is_visible(), which on DB9 is true for every schema. See
ORM schema and migration tooling.
Migrations are unaffected — makemigrations reads your models, not the database, and migrate
only issues DDL.
SSL connection errors
Section titled “SSL connection errors”Ensure sslmode=require is set. If using the direct config, add it under OPTIONS:
"OPTIONS": { "sslmode": "require",}psycopg2 not found
Section titled “psycopg2 not found”Install the binary package: pip install psycopg2-binary. For production, compile from source with pip install psycopg2 (requires libpq-dev).
Next Pages
Section titled “Next Pages”- SQLAlchemy — Python ORM alternative with SQLAlchemy 2.0
- Connect — connection strings and authentication
- Production Checklist — deployment readiness