Skip to main content

Revisions and Migrations

FrankPHP is intentionally simple to ship: a versioned Zip, a small set of framework files, and clear context files that explain how the framework behaves. That simplicity does not mean every upgrade should be treated as a file overwrite. A FrankPHP revision can contain two different kinds of change:
  1. changes that affect new installs only
  2. changes that must be applied carefully to existing applications with live data
This page explains how to handle that distinction.

The core rule

Treat every FrankPHP release as two related but separate deliverables:
  • the framework revision: files, docs, conventions, route changes, service changes, schema templates
  • the migration path: the deliberate steps needed to bring an existing application forward safely
Do not replace a live application’s database schema wholesale with the fresh-install schema from a new framework release. A fresh-install schema describes what a new project should start with. A migration script describes how an existing project should move forward.

What counts as a revision?

A revision is the complete framework state for a released version. For FrankPHP, that normally includes:
  • VERSION
  • CHANGELOG.md
  • CODEBASE.md
  • RELEASE_MANIFEST.json
  • framework-owned PHP files
  • sql/schema.sql
  • any migration scripts for existing installs
  • updated documentation
The revision answers one question:
What is true about FrankPHP at this version?
The migration answers a different question:
How does an existing application safely become compatible with this version?
Both are needed.

Fresh install versus existing install

A fresh install has no customer data yet. It can use the latest framework schema directly. An existing install already has users, tenants, settings, timestamps, and application-owned tables. That data must be preserved. Use this decision table:
SituationUse
Starting a brand new FrankPHP projectlatest framework Zip and fresh sql/schema.sql
Upgrading an existing apprelease notes plus the version-specific migration script
Comparing framework behaviourCHANGELOG.md, CODEBASE.md, and RELEASE_MANIFEST.json
Updating AI context for future coding sessionslatest CODEBASE.md plus the app’s current MYAPP.md

The v1.3.0 example

FrankPHP v1.3.0 introduced the framework’s first formal date/time standard:
UTC at rest. Local at the edges.
It also introduced App\Core\Clock, the framework-owned utility for UTC timestamp generation, timezone resolution, UTC/local conversion, and local-date query boundary calculation. That change affects both new installs and existing applications. For a new install, the corrected v1.3.0 schema can declare framework timestamp columns as UTC DATETIME from the start. For an existing install, the migration needs to inspect the current schema, guard against duplicate identity data, convert compatible TIMESTAMP columns safely, and avoid making assumptions about historical DATETIME values.
Follow this workflow for each FrankPHP release.

Step 1: Read the changelog entry

Start with CHANGELOG.md. Look specifically for:
  • added framework utilities
  • changed framework rules
  • database changes
  • migration notes
  • build rules introduced for AI-assisted development
For v1.3.0, the important rule is that persisted timestamps should be generated in UTC through App\Core\Clock, while local timezones are used only at input, output, and query-boundary edges.

Step 2: Classify the release

Ask what kind of release this is.
Release typeWhat to look for
Documentation-onlydocs, examples, context files
Framework behaviourcontrollers, services, middleware, core utilities
Schema-affectingsql/schema.sql, framework tables, indexes
Data-affectingexisting values must be transformed, normalised, or reviewed
Application-contract affectingCODEBASE.md rules that AI agents must now follow
v1.3.0 is a framework hygiene release, but it is also schema- and data-relevant because it formalises timestamp storage expectations and migration handling for MySQL timestamp columns.

Step 3: Update framework files deliberately

Framework-owned files should be updated as a versioned framework upgrade, not mixed into ordinary application feature work. Typical framework-owned files include:
  • Core/Clock.php
  • framework models such as Models/User.php and Models/Tenant.php
  • framework auth/signup services
  • bootstrap.php
  • sql/schema.sql
  • CODEBASE.md
  • CHANGELOG.md
FrankPHP deliberately separates framework-owned files from application-owned files. Application features should continue to be documented in MYAPP.md; framework conventions belong in CODEBASE.md.

Step 4: Run migration preflight checks

Before changing a live database, run the release’s preflight checks. For v1.3.0, one important preflight check is platform-wide email uniqueness:
SELECT email, COUNT(*) AS duplicate_count, GROUP_CONCAT(id ORDER BY id) AS user_ids
FROM users
GROUP BY email
HAVING COUNT(*) > 1;
If this returns rows, stop and resolve them before adding or enforcing a global UNIQUE(email) index. This is a product/account ownership decision, not a technical detail the migration should guess.

Step 5: Run the version-specific migration script

Use the migration script for the release, not the fresh-install schema. For v1.3.0, existing applications should use:
migration_v_1.3.0.sql
The migration should be designed to handle existing installs carefully. For v1.3.0, that means:
  • detecting duplicate emails before enforcing global identity uniqueness
  • adding missing framework columns only when required
  • standardising the database session timezone during timestamp conversion
  • converting compatible framework TIMESTAMP columns to DATETIME
  • producing post-migration review queries

Step 6: Treat historical timestamps carefully

MySQL DATETIME stores a date and time but no timezone metadata. That means a migration cannot automatically know whether an old value was:
  • already UTC
  • local wall-clock time
  • generated by PHP on a server using local timezone
  • generated by SQL using the database session timezone
If historical values were stored as local wall-clock values, correct them deliberately using the known source timezone. Example:
UPDATE some_table
SET created_at = CONVERT_TZ(created_at, 'Europe/London', '+00:00')
WHERE created_at IS NOT NULL;
Only run a conversion like this when you know the previous values were stored in that local timezone. Do not apply timezone conversion blindly.

Step 7: Update application code to follow the new contract

For v1.3.0, search application code for direct date/time usage:
date(
time(
strtotime(
new DateTime(
new DateTimeImmutable(
NOW()
CURRENT_TIMESTAMP
Then replace persisted timestamp generation, local-date filters, and display formatting with App\Core\Clock patterns. The goal is not to make every line of date code look identical. The goal is to make the responsibility boundary consistent:
ConcernBelongs where
persisted timestamp generationapplication/service/model code using Clock
timezone resolutionservice/controller boundary using explicit user/tenant/config context
local input conversionservice/controller before storage
UTC query range calculationservice/model before SQL query
display-ready date labelsservice/controller before rendering
raw formatting or conversion in viewsnot allowed

Step 8: Update MYAPP.md

If the application has date/time behaviour, record it in MYAPP.md. Useful things to document include:
  • which application tables store UTC DATETIME values
  • which columns are intentionally DATE
  • any historical timestamp corrections made during migration
  • any deliberate deviation from the framework rule
  • any application-specific timezone source beyond user, tenant, config, and UTC fallback
A deliberate deviation can be documented. A view performing timezone conversion should not be treated as a deviation. It should be refactored.

What to keep with each release

A good FrankPHP release folder should leave you with enough information to answer three questions later:
  1. What changed?
  2. Why did it change?
  3. How did existing installs move safely?
For v1.3.0, keep:
  • the release Zip
  • CHANGELOG.md
  • CODEBASE.md
  • the fresh-install schema
  • migration_v_1.3.0.sql
  • notes from any project-specific timestamp correction
  • the updated application MYAPP.md
That is enough for a human developer or an AI assistant to understand the upgrade path without guessing.

Practical release checklist

Use this as the short version before upgrading an existing app.
  • Read the release changelog
  • Identify framework-owned file changes
  • Identify schema and data changes
  • Back up the database
  • Run preflight checks
  • Resolve duplicate identity records before migration
  • Run the version-specific migration script
  • Review post-migration output
  • Search application code for old date/time patterns
  • Replace persisted timestamp writes with Clock
  • Replace SQL local-date filters with UTC boundaries
  • Keep views free from timezone conversion
  • Update MYAPP.md

Conclusion

FrankPHP upgrades should stay simple, but they should not be casual. The safest approach is to keep the framework revision, fresh-install schema, and existing-install migration path separate. That lets FrankPHP stay lightweight while still protecting real application data as the platform evolves.