Skip to content
Discord Get Started

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.

  • A DB9 database (create one)
  • Python 3.10+
  • Django 4.2+
Terminal
db9 create --name django-app

db9 create prints the connection string, with the admin password, once — copy it from the Connection String: line. It has this shape:

Output
postgresql://YOUR_TENANT_ID.admin:YOUR_PASSWORD@pg.d0000.db9.io:5432/postgres

YOUR_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:

.env.local
DATABASE_URL="postgresql://YOUR_TENANT_ID.admin:YOUR_PASSWORD@pg.db9.io:5433/postgres?sslmode=require"

Install dependencies:

Terminal
pip install django psycopg2-binary dj-database-url python-dotenv
settings.py
import dj_database_url
from 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,
)
}
blog/models.py
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.title

Generate and apply migrations:

Terminal
python manage.py makemigrations blog
python manage.py migrate

Verify the tables exist:

Terminal
python manage.py dbshell
\dt
blog/views.py
from django.http import JsonResponse
from .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)
blog/urls.py
from django.urls import path
from . 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"),
]
myproject/urls.py
from django.urls import path, include
urlpatterns = [
path("api/", include("blog.urls")),
]

For a full API layer, add Django REST Framework:

Terminal
pip install djangorestframework
blog/serializers.py
from rest_framework import serializers
from .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"]
blog/views_api.py
from rest_framework import viewsets
from .models import User, Post
from .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 = PostSerializer
blog/urls.py
from django.urls import path, include
from rest_framework.routers import DefaultRouter
from .views_api import UserViewSet, PostViewSet
router = DefaultRouter()
router.register("users", UserViewSet)
router.register("posts", PostViewSet)
urlpatterns = [
path("api/", include(router.urls)),
]
  • 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_AGE to reuse connections across requests. For high-traffic apps, consider django-db-connection-pool or PgBouncer.
  • Host and port: pg.db9.io:5433 and the pg.d0000.db9.io:5432 that db9 create prints reach the same server; port 5432 works on either host.
  • TLS: Always include sslmode=require in the connection string or set it in OPTIONS for DB9’s hosted service.
  • WSGI server: Use Gunicorn or uWSGI in production. Django’s development server (runserver) is not designed for production traffic.

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.

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 has job, job_run_details and running_jobs in the cron schema. inspectdb lists them next to your own tables and then cannot read them:

    # Unable to inspect table 'job'
    # The error was: relation "job" does not exist

    These are comments — the command still exits 0 and the rest of the file is valid Python. Delete them, or name the tables you want: python manage.py inspectdb users posts.

  • CharField comes back without max_length. A VARCHAR(100) column is generated as models.CharField(). Add the lengths by hand; information_schema.columns reports them in character_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.

Ensure sslmode=require is set. If using the direct config, add it under OPTIONS:

Python
"OPTIONS": {
"sslmode": "require",
}

Install the binary package: pip install psycopg2-binary. For production, compile from source with pip install psycopg2 (requires libpq-dev).