Upgrading from 1.5 to 2.0
Raytha 2.0 upgrades a 1.5 database in place. The upgrade changes the schema and also rewrites some of your data, and the data changes cannot be undone by a migration rollback. Take a backup first; restoring it is the way back.
Before you start
- Be on PostgreSQL and on 1.5. The schemas of 1.5.0, 1.5.1 and 1.5.2 are identical, so any 1.5.x database upgrades the same way. From 1.4.x, bring the database to 1.5.0 first with
db/Postgres/v1_4_0_to_v1_4_1.sqlanddb/Postgres/v1_4_1_to_v1_5_0.sqlfrom the repository. - SQL Server has no upgrade path. 2.0 supports only PostgreSQL. Move your data to PostgreSQL while still on 1.5.0, then upgrade.
- Back up the database and the file storage. See Backups and restores. For a Docker setup:
docker compose exec -T db pg_dump -U postgres -Fc raytha > raytha-1.5-before-2.0.dump - Check the Website URL under Settings, Configuration. 2.0 builds media links and email links from it. A stale value, such as an old domain, breaks images in API responses and links in emails.
Run the upgrade
Choose one. Both end at the same schema.
Option A: let Raytha migrate
Start 2.0 with APPLY_PENDING_MIGRATIONS=true. The setting is on in appsettings.json and in the Compose examples in these docs. Raytha applies the single 2.0 migration, 20260921002825_v2_0_0, then starts. With the published image, change the tag to raythahq/raytha:2.0.0 in your Compose file and pull:
docker compose pull app
docker compose up -d app
docker compose logs -f app
If you build from the repository instead, git pull and run docker compose --env-file .env up -d --build.
Option B: apply the SQL script yourself
Use this when you want to see the output, or when the app does not have permission to change the schema. Stop the 1.5 app, run the script, then start 2.0 with APPLY_PENDING_MIGRATIONS=false.
psql -v ON_ERROR_STOP=1 -h localhost -U postgres -d raytha \
-f db/Postgres/v1_5_0_to_v2_0_0.sql
Against the Compose database:
docker compose exec -T db psql -v ON_ERROR_STOP=1 -U postgres -d raytha \
< db/Postgres/v1_5_0_to_v2_0_0.sql
The script runs in one transaction. If any statement fails, nothing is applied. On success its last statement records the migration in __EFMigrationsHistory, so a later start with APPLY_PENDING_MIGRATIONS=true has nothing left to do. Read the output in psql: it is the only place the date notices described below appear.
Schema changes
- New tables:
EmailLogs,Webhooks,WebhookDeliveries,UserWebTemplate. OrganizationSettingsgainsAuditLogRetentionDays,BackgroundTaskRetentionDays,EmailLogRetentionDaysandWebhookDeliveryRetentionDays, each defaulting to 180.WidgetTemplatesandWidgetTemplateRevisionsgain a fields definition (_FieldsJson), andContentTypeFieldsgains sub-field definitions.RoutesandRaythaFunctionsare linked to each other, so a function can serve an HTTP route.AuditLogsgainsImpersonatorEmail.- New indexes.
Data rewrites
These happen during the upgrade, whichever option you use.
| What changes | Details |
|---|---|
| Date fields become ISO dates | 1.x stored dates as m/d/yyyy or in the server's culture. Each value is rewritten to YYYY-MM-DD, or YYYY-MM-DDTHH:MM:SS if it had a time. This covers published content, drafts, revisions and the trash. |
| Media links become root-relative | Links such as http://localhost:5200/raytha/media-items/objectkey/abc_x.png in rich text and page-builder widgets lose their scheme and host, but only when the object key exists in this database's media library. Covers content, drafts, revisions, trash, site pages and site page revisions. Links to other sites and all templates are left alone. |
| Manage Media is a separate permission | Any role that has Manage System Settings, Manage Content Types, or Edit on any content type is granted Manage Media, so nobody loses access to the media library. Other roles do not get it. |
| Magic-link sign-in uses a one-time code | The email template raytha_email_login_beginloginwithmagiclink and the page template raytha_html_login_magiclinksent are replaced with code-based versions, unless they already use the code. Your previous content is saved as a revision first. Magic-link emails sent before the upgrade stop working. |
| Built-in widget templates get field definitions | The templates hero, wysiwyg, imagetext, card, faq, cta, embed and contentlist (and their revisions) get their field definitions for the 2.0 page builder. Only templates with an empty field list are touched; your own widget templates are not. |
How dates are converted
For each date field, the script infers the day/month order from that field's own values: a first number above 12 means day-first. Then it rewrites every value in the field.
| Stored in 1.5 | After upgrade | Why |
|---|---|---|
3/15/2024 | 2024-03-15 | 15 cannot be a month, so month-first. |
1/2/2024 3:45:00 PM | 2024-01-02T15:45:00 | Month-first in this field; the time is kept. |
31/12/2023 and 4/5/2024 in the same field | 2023-12-31 and 2024-05-04 | The 31 makes the field day-first, which settles the ambiguous value. |
not a date | unchanged | Unrecognised values are left as they are and reported. |
A value that could be read either way, with nothing else in its field to decide, is also left unchanged. Each such field produces a notice such as Date values left unchanged in events.starts: unrecognized format (1 values). Only psql shows it. Option A hides it, so run this query after any upgrade to list what was left behind:
SELECT t."DeveloperName" AS content_type, f."DeveloperName" AS field, ci."Id",
ci."_PublishedContent" ->> f."DeveloperName" AS value
FROM "ContentItems" ci
JOIN "ContentTypes" t ON t."Id" = ci."ContentTypeId"
JOIN "ContentTypeFields" f ON f."ContentTypeId" = ci."ContentTypeId" AND f."FieldType" = 'date'
WHERE coalesce(ci."_PublishedContent" ->> f."DeveloperName", '') !~ '^([0-9]{4}-[0-9]{2}-[0-9]{2}.*)?$';
2.0 reads a leftover value in the server's culture, as 1.x did. A value it cannot parse reads as an empty date, and the admin editor shows the stored text and asks you to enter the date it meant.
Check the result
- Open
/healthz/ready. Both checks should beHealthy. See Health checks. - Sign in at
/raytha. The admin is now a React app that replaces the Razor admin. - Run the leftover-dates query above and fix any rows in the admin.
- Open a few public pages that show images or dates, and look at the HTML. Root-relative media links resolve against the site, so a wrong Website URL shows up here.
- Send yourself a sign-in code or a password recovery email. See Sending emails.
- If you use the REST API, check a response with an image. API responses turn root-relative media links back into absolute ones using the Website URL, so existing consumers still get full URLs.
Roll back
Restore the backup you took before the upgrade, and run the 1.5 version of the app against it. Running the 1.5 app against an upgraded database is not supported: it cannot read the converted dates, and the migration cannot be reversed because the data rewrites are one-way.
docker compose stop app
docker compose exec -T db dropdb -U postgres raytha
docker compose exec -T db createdb -U postgres raytha
docker compose exec -T db pg_restore -U postgres -d raytha --no-owner < raytha-1.5-before-2.0.dump
Restore the file storage backup as well if media was uploaded after the upgrade and you want the two to match.
Gotchas
- Webhooks, the email log and the new retention settings start empty. Nothing is migrated into them.
- The script only rewrites media links whose object key is in this database's
MediaItems. A link to an uploaded file whose media row was deleted keeps its absolute URL. - Customised copies of the built-in magic-link templates are replaced too, unless they already contain
Target.Codeormagic-link/complete. Look in the template's revisions to merge your edits back. - Running Option A on a database that already had the script applied does nothing, because the migration is recorded.
- Do not edit
__EFMigrationsHistoryby hand to retry an upgrade. Restore the backup and start again.
Next steps
- What is new in 2.0.
- Webhooks and Health checks, both new in 2.0.
- Configuration, including the new proxy-trust settings.