Skip to main content
An app is one row with a home storefront. Keywords are separate rows scoped by text, store and country, and a join table connects the two. That shape is what lets a single app track keywords in many markets without duplicating the app.

Entities

How do they relate?

A workspace owns apps. Each app captures snapshots of its listing and tracks keywords through a join table, because the same keyword row is shared by every app that tracks it. Popularity and difficulty attach to the keyword, while a position attaches to the pair of an app and a keyword on a given date. Action items belong to the workspace, and each one records its lifecycle as events: one row per transition, such as opened, done or confirmed fixed, deleted together with the action.

Rules that are easy to get wrong

  • An app is one row. Countries live on keyword tracking, not on a second app row. See Countries and markets.
  • Keyword is unique on text, store and country. The same phrase tracked in two markets is two keyword rows checked by two searches.
  • A competitor is an app row too. It carries isCompetitor and points at the primary app it was added to.
  • Popularity and difficulty persist in KeywordMetric. Opportunity does not persist. It is computed in the read layer from the stored popularity and difficulty. See Popularity, difficulty and opportunity.
  • installs is a BigInt and serializes as a JSON number.
  • All dates are UTC. Daily granularity uses a PostgreSQL date column, and today means the UTC date.
  • Every tenant owned row carries workspaceId, and PostgreSQL enforces it. Each of those tables has a tenant_isolation row level security policy, so a query with no workspace in scope returns nothing rather than everything. Service code does not filter by workspace in a where clause. Keyword, KeywordMetric and SerpEntry are deliberately global and carry no policy, because one search for a phrase serves every workspace tracking it. ScreenshotText is global for the same reason: a screenshot is public store data, so one read serves every workspace that tracks the app. What is tenant owned is the tracking relation and the recorded position.

How does the schema change?

Only forward. Every schema change ships as a forward Prisma migration and the API applies pending migrations on boot. There are no hand written SQL migrations and no down migrations, which is why a rollback is a database restore. See Upgrade and roll back.
Last modified on October 24, 2026