Short answer: you cannot. v2 is a fresh install, not an upgrade.
There is no migration script, and this document explains why rather than apologising for it. If you are running v1 today, nothing here forces you to move — v1 remains supported on its own branch and tag, and the two deployments share nothing.
What changed, and why it is not a migration
v2 is a rebuild, not a version bump. Four of the five load-bearing dependencies were replaced:
| Concern | v1 | v2 |
|---|---|---|
| Identity | Kinde | Better Auth + the organization plugin |
| File storage | UploadThing | Any S3-compatible provider |
| Billing | Lemon Squeezy | Polar, and optional |
| API | Route handlers | tRPC with a procedure ladder |
| Tenancy | Ad hoc, per query | Structural — ctx.orgId, lint-enforced |
Three of those are survivable in a migration. The identity change is not, and it is the one that decides the answer.
The blocker: Kinde identities cannot become Better Auth users
A Kinde user is a record in Kinde. Your v1 database holds a foreign key to it and nothing else that can authenticate anybody:
- No password hashes. Kinde holds the credential; v1 never had it. There is nothing to copy into Better Auth's
accounttable, and nothing to derive one from. - No usable social links. A Kinde-brokered Google identity carries Kinde's subject identifier, not Google's. Writing it into Better Auth's
accountrow would produce a link that never matches on the next sign-in. - No sessions. Everyone is signed out at the cut-over regardless.
So the best possible automated migration still ends with every user resetting their password by email — which is the same user-visible event as "we moved, please sign up again", with the added risk of a half-migrated identity table nobody can debug afterwards.
Given that, migrating the identity rows buys nothing and costs the ability to say what state a user is in.
The second problem: the data model moved underneath
Even setting identity aside, v1's rows do not map cleanly onto v2's:
TaskStatuswas a fixed enum.WorkflowStatusis a per-project row with a name, a colour, a position, and a category. A v1 task's status is a string that has to be matched against statuses that do not exist yet, per project.- Comments in v1 could attach to a project or a task. In v2
taskIdis required — that mismatch was one of the bugs the rebuild set out to fix. A project-level v1 comment has nowhere to go. - Task descriptions were plain text. In v2 they are TipTap documents with a derived plain-text twin feeding the search index.
- Projects gained
visibilityandProjectMember. Every v1 project has to be assigned a visibility, and the safe default (RESTRICTED) is the one that makes projects disappear for most of your team until somebody adds members. - v1's time-tracking and analytics tables have no destination. Both features are deliberately out of scope for v2.0.0.
- Users, organizations, projects, and tasks all changed primary-key generation — v2 uses UUIDv7 for its own ids.
Each of those is a decision somebody has to make per row, and a wrong guess is silent.
What "fresh install" actually means
- Deploy v2 as a new application, with its own database and its own credentials. Follow SETUP.md.
- Your team signs up, creates an organization, and invites each other. Roles are assigned once, deliberately.
- Recreate the projects that are still live. Most teams find this is a dozen projects and an afternoon, and that the archive they didn't recreate was the point.
- Keep v1 running read-only for as long as you need the history.
Do not point v2 at v1's database. The migrations will not apply cleanly onto v1's schema, and a partial apply is worse than a failure.
Keeping v1 alive alongside
v1 is on the legacy/v1 branch at tag v1.0.0 and deploys independently. Two rules:
- Separate deployment projects. No v1 deployment should hold a Better Auth, Polar, or S3 variable; no v2 deployment should hold a Kinde, UploadThing, or Lemon Squeezy variable.
- Separate databases. They share no schema and no rows.
v1 continues to receive patches as v1.0.x. It will not receive a stack change.
If you genuinely need the data moved
It is possible; it is just not a script we can write for you, because the decisions are yours. A realistic path:
- Export from v1 — projects, tasks, comments, and the user list, as JSON straight out of Postgres.
- Decide the mappings. For each v1 project: which of the six default statuses does each v1 status become? Is the project
OPENorRESTRICTED, and who are its members? What happens to project-level comments? - Have everyone sign up in v2 first. Then build an email → v2
user.idmap from the real table. This is what removes the identity problem: the users are real, created by the normal flow, and authenticate normally. - Import through the services, not through SQL.
projectService.create,taskService.create, and so on take(ctx, input)and are callable from a script — that is the entire reason they are pure functions of their arguments. Going through them means limits, activity events, ranking, and the search index are all maintained. Raw inserts skip all four, and the missingtsvectoris the one you notice last. - Reassign by email, mapping v1 assignees onto v2
Memberids. Note thatTask.assigneeIdpoints at aMember, never aUser— a member row belongs to exactly one organization, so a foreign-tenant assignee is unrepresentable.
EXTENDING.md shows how to call a service from outside a request, and prisma/seed.ts is a working example of a script that writes a complete workspace.
If you would like this done for you, it is a paid engagement rather than a download — the mapping decisions above are the work, and they are different for every installation. The contact address is in .package.json
Upgrading within v2
Ordinary and safe:
git pull
pnpm install
pnpm db:generate
pnpm exec prisma migrate deploy
pnpm build
Read the release notes for the version you are moving to. If you have modified the codebase — which you are licensed to do, and which is the point — git pull may conflict; the layer rule is what keeps those conflicts local, because a feature you added lives in its own service, router, and component files rather than threaded through shared ones.