Moving to a new database server does not mean starting from scratch. Version 1 had production data — journal entries, projects, job history — that needed to carry over to the new Supabase PostgreSQL instance. The approach: export from the live v1 environment, adapt for the v2 schema, and load everything into the new database using Django management commands.
The v1 production database was running on Render. Using the Render CLI with an SSH key, I connected to the running instance and exported the data as JSON. Render gives you a shell into your running container, so standard Django management commands work directly against the live database.
The exported JSON uses Django's serializer format — each object is wrapped under a "fields" key, with its primary key preserved. Keeping the original PKs matters for data that references other records by ID (links referenced by journal entries, platforms referenced by links).
Not all data came from the production export. The profile, achievements, and job history were rewritten from scratch — the v2 model structure changed significantly enough that the old records were not worth adapting. These files use a clean, handwritten format with multilingual keys:
{
"Skill": [{
"name": {
"en": "Commitment to quality",
"de": "Qualitätsbewusstsein",
"fr": "Exigence de qualité"
}
}]
}
The journal entries, links, and projects were migrated from the production export and retain Django's "fields" wrapper. Each command accounts for whichever format its data file uses:
# new format — direct access
for entry in data["Skills"]:
name = entry["name"]["en"]
# exported format — unwrap fields first
for entry in data["Journals"]:
fields = entry["fields"]
name = fields["name"]
Every command uses get_or_create instead of create. This makes them safe to run multiple times — on first run they create; on subsequent runs they skip existing records and report it:
journal, created = Journal.objects.get_or_create(
name=fields["name"],
defaults={
"status": fields.get("status", "DF"),
"content": fields["content"],
"category": fields["category"],
"published": fields.get("published"),
},
)
if created:
self.stdout.write(self.style.SUCCESS(f"Created: '{journal.name}'"))
else:
self.stdout.write(f"Skipped (exists): '{journal.name}'")
This also means the commands double as a recovery tool: if the database is wiped and re-migrated, running a single command restores all data.
Journal entries should appear in their original order from v1. Django's auto_now_add makes created and updated unwritable through the normal save path — they are always set to now. The workaround is a direct .update() call after creation, which bypasses the field's auto behaviour:
if created:
Journal.objects.filter(pk=journal.pk).update(
created=fields["created"],
updated=fields["updated"],
)
The profile, achievements, and jobs store content in three languages. Each command maps each language explicitly to its model field — django-modeltranslation generates separate database columns (name_en, name_de, name_fr) for each translated field:
Language.objects.create(
name_en=language["name"]["en"],
name_de=language["name"]["de"],
name_fr=language["name"]["fr"],
...
)
data_create.py calls every command in sequence using Django's call_command(). Order is important — some commands depend on records created by earlier ones:
call_command("createsuperuser", username="Doro", email="...")
call_command("profile_create")
call_command("achievements_create")
call_command("degrees_create")
call_command("jobs_create")
call_command("languages_create")
call_command("social_media_create")
call_command("platform_create") # before links
call_command("links_create") # before journals
call_command("journals_create")
call_command("skills_create") # before projects
call_command("projects_create") # uploads images to Supabase
Running python manage.py data_create on a fresh database seeds everything in one step — user, profile, all content, and project images uploaded to Supabase storage. The same command runs on Render after the first deploy.