Cytoskeleton is a Gradle plugin for PostgreSQL databases. It gives your project two commands:
generateMigration– makes a new, empty SQL migration file for you (with a helpful starting template if you want one).runMigration– reads your migration files and runs them against your PostgreSQL database. It remembers which ones it already ran, so it never runs the same migration twice.
A "migration" is just a .sql file that changes your database (for example, creating a table
or adding a column). Keeping these changes in files means your database history lives in your
project, next to your code.
The plugin id is com.outpx.cytoskeleton.
- A JDK (Java) version 21. Gradle uses it to run the plugin. If you don't have Java 21, Gradle can download it for you automatically (this project is already set up for that).
- A running PostgreSQL database that you can connect to. The plugin does not create a database for you — it connects to one that already exists.
You do not need to install any extra libraries (like a PostgreSQL driver). The plugin already includes everything it needs. Adding the plugin is the only step.
Open the build.gradle.kts file of the project where you want migrations and add the plugin:
plugins {
id("com.outpx.cytoskeleton") version "0.1.0"
}That single line is all it takes. After this, the two commands (generateMigration and
runMigration) are available in that project.
If the plugin has not been published yet, you can still use it directly from its source folder.
In your project's settings.gradle.kts, tell Gradle where the plugin lives:
// settings.gradle.kts
pluginManagement {
includeBuild("/path/to/cytoskeleton") // the folder that contains this plugin
}Then in build.gradle.kts, add the plugin without a version number (the version is not
needed when you point at the source folder):
// build.gradle.kts
plugins {
id("com.outpx.cytoskeleton")
}There is a full working example in the example/ folder of this repository.
The plugin reads your database details from a file called application.properties. By default,
the plugin looks for it here:
src/main/resources/application.properties
Create that file and fill in your database details:
cyto.db.host=localhost
cyto.db.port=5432
cyto.db.name=example
cyto.db.credentials.username=postgres
cyto.db.credentials.password=postgresHere is what each line means:
| Property | Meaning |
|---|---|
cyto.db.host |
Where your database is (for example, localhost). |
cyto.db.port |
The port your database listens on (PostgreSQL usually uses 5432). |
cyto.db.name |
The name of the database to connect to. |
cyto.db.credentials.username |
The username to log in with. |
cyto.db.credentials.password |
The password to log in with. |
All five properties are required. If any are missing, runMigration stops and tells you which
one is missing.
You usually do not want to write real passwords directly into application.properties, because
that file is often shared or committed to git. Instead, you can use a placeholder written as
${SOMETHING} and keep the real value in a separate .env file.
For example, in application.properties:
cyto.db.credentials.username=${DB_USER}
cyto.db.credentials.password=${DB_PASSWORD}And in a .env file at the top of your project:
DB_USER=postgres
DB_PASSWORD=my-secret-password
When you run a migration, the plugin swaps each ${DB_USER} placeholder for the matching value.
It looks for that value in two places, in this order:
- The nearest
.envfile (the plugin searches the current folder, then its parent folders, until it finds one). - Your computer's environment variables (what you would see with
echo $DB_USER).
If a placeholder cannot be found in either place, runMigration stops and lists the missing
names, so you know exactly what to fix. Remember to add .env to your .gitignore so your
secrets are never committed.
Run this command from your project folder:
./gradlew generateMigration -PmigrationName=CreateUsers-PmigrationName=CreateUsersis required. It is the name of your migration. Pick something that describes what the migration does (for example,CreateUsers,AddEmailColumn).
This creates a new file inside your migrations folder (by default
src/main/resources/migrations/). The file name starts with a timestamp so files always run in
the order you created them, for example:
src/main/resources/migrations/1787428750594_CreateUsers.sql
The timestamp number in front is added automatically. You then open that file and write your SQL.
If you are creating a new table, you can ask the plugin to fill the file with a ready-made
starting template by adding -PtableName:
./gradlew generateMigration -PmigrationName=CreateUsers -PtableName=usersThis writes a CREATE TABLE users (...) statement, plus a few helpful database triggers, into
the new file for you. By default the table starts with these columns:
id UUID PRIMARY KEY NOT NULL DEFAULT uuid_generate_v4(),
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()It also adds triggers that:
- automatically update the
updated_atcolumn whenever a row changes, and - send a PostgreSQL notification whenever a row is created, updated, or deleted (useful if something in your app wants to listen for changes).
If you leave out -PtableName, you simply get an empty file to write whatever SQL you like.
Run this command:
./gradlew runMigrationHere is exactly what happens when you do:
- The plugin finds every
.sqlfile in your migrations folder and sorts them by name (which, because of the timestamp, means oldest first). - It reads your
application.propertiesand fills in any${...}placeholders from your.envfile or environment variables. - It connects to your PostgreSQL database using those details.
- The first time it runs, it sets up a small bookkeeping table called
applied_migrations. This is how it remembers which migrations have already been applied. - It runs each migration that has not been applied before. Migrations that were already applied are skipped. This means running the command again is safe — it will not re-run old migrations.
If there are no migration files, the command simply says there is nothing to do and finishes.
Everything above works with the default folder locations, so you may never need this section.
But if you want to change where files live or what the template looks like, you can add settings
to your build.gradle.kts.
There are two settings blocks:
migrations { }– controls file locations and how migrations run.generateMigration { }– controls the table template used bygenerateMigration.
migrations {
// Folder where migration files are created and read from.
migrationsDir.set(layout.projectDirectory.dir("src/main/resources/migrations"))
// The file that holds your database connection details.
applicationProperties.set(layout.projectDirectory.file("src/main/resources/application.properties"))
// Folder where the search for a `.env` file begins.
searchStart.set(layout.projectDirectory)
// Extra SQL that runs once, before your migrations. By default this sets up helper
// functions the templates rely on (like uuid generation and the notification triggers).
// Only change this if you know you need something different.
initSql.set("-- your setup SQL here")
}
generateMigration {
// The columns placed inside a new CREATE TABLE when you use -PtableName.
tableSqlContent.set(
"""
id UUID PRIMARY KEY NOT NULL DEFAULT uuid_generate_v4(),
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
""".trimIndent()
)
}If you do not set migrationsDir yourself, the plugin picks a sensible default:
- If your project uses a JVM plugin (like
javaor the Kotlin plugin), it uses that project's main resources folder, usuallysrc/main/resources/migrations. - If your project does not use any such plugin, it falls back to
src/main/resources/migrations.
You can apply a JVM plugin in any order (before or after this plugin) — it does not matter.
| Command | What it does |
|---|---|
./gradlew generateMigration -PmigrationName=Name |
Create an empty migration file. |
./gradlew generateMigration -PmigrationName=Name -PtableName=table |
Create a migration file pre-filled with a CREATE TABLE template. |
./gradlew runMigration |
Apply all migrations that have not run yet. |