github →

New request

#20203: App manifest asset URLs (logo, screenshots) are inconsistently resolved across deploy paths

We'll provision a sandbox, run an agent against the issue, and open a draft PR. You can pull the branch and iterate from there.

Issue
sonarly:mediumApps

Problem

applicationRegistration.manifest.application.logoUrl (and screenshots) has three different resolutions depending on how the app was deployed, only one of which actually works for the front:

SourceWhat ends up in manifest.logoUrlBrowser-loadable?
NPM-published (catalog sync via marketplace-catalog-sync.service.ts)Absolute CDN URL (buildRegistryCdnUrl(...))
Dev mode (syncApplicationapplication-development.resolver.syncRegistrationMetadata)<SERVER_URL>/public-assets/<workspaceId>/<applicationId>/<path> baked in at sync time⚠️ — works for the deploying workspace only; broken for any second workspace installing the same registration
Tarball deploy (application-tarball.service.ts)Raw manifest path (e.g. public/linear-logomark.svg)❌ — front 404s

The user-visible symptom: logos don't render for tarball-deployed apps. This is what we just hit on the twenty-linear internal app — the SVG is uploaded correctly to FileFolder.PublicAsset, but the manifest still points at the SDK-relative path public/linear-logomark.svg, which the front blindly puts in an <img src>.

Root cause

A registration's manifest is install-agnostic (one registration → potentially N installs across N workspaces), but the public-assets URL is install-scoped (/public-assets/:workspaceId/:applicationId/*path). The two aren't compatible: you cannot bake a workspace+install-scoped URL into shared registration data and expect it to work for the second workspace that installs the same registration.

The dev path silently gets away with mutating the registration manifest because dev = one workspace per registration. Tarball doesn't bother trying. Only NPM is correct, and only because CDN URLs happen to be install-agnostic.

There's also a layering issue: a previous attempt at fixing this (#20181 commit b81c51c4, since reverted) added workspace-aware URL rewriting inside findMarketplaceAppDetail. That's wrong — it's a pre-install browse query, the workspace context shouldn't matter to it. The hack made tarball logos work in the installed-app settings page but conceptually broke the resolver's contract.

Front-side resolution chain (today)

SettingsApplicationDetails.tsx:94-98:

```tsx const logoUrl = app?.logoUrl ?? // manifest (raw path for tarball, broken) application?.logo ?? // install record (set to raw path at install time, also broken) application?.applicationRegistration?.logoUrl ?? undefined; ```

The first non-undefined wins. For tarball apps app?.logoUrl is the raw path, so the chain stops there and the browser tries to load <workspace>/public/linear-logomark.svg.

Options

Option A — Resolve at install time, store on the install record

  • In application-install.service.ts (line ~146), compute the absolute URL \${serverUrl}/public-assets/${workspaceId}/${application.id}/${manifestPath}`and store it asapplication.logo`.
  • Front: drop app?.logoUrl from the resolution chain; read only application.logo.
  • Cleanup: dev resolver stops mutating the registration's manifest; mirrors install-time resolution.

Pros: small change, matches the existing NPM pattern ("store the resolved URL once it's been deployed/installed in some scope"), reuses the existing application.logo column, fully reversible.

Cons: derived URL persisted on the install record (revisit if SERVER_URL changes — but same risk exists today for NPM CDN URLs in the registration manifest). Doesn't help with the screenshots field (would need a sibling screenshotUrls column or a JSON field).

Option B — Resolve at read time on the install record's GraphQL DTO (recommended)

  • Add @ResolveField() logoUrl: string (and screenshots) to ApplicationDTO (the install record). The resolver knows the install record's id and workspaceId, builds the URL on demand from the registration's raw manifest path.
  • Drop application.logo column entirely (it was always derived).
  • Manifest stays raw / canonical — applicationRegistration.manifest.logoUrl is \"public/...\" always, no per-deploy mutation, no per-install mutation.
  • findMarketplaceAppDetail and findManyMarketplaceApps continue to expose the raw path; the install resolver is the single boundary that converts to a usable URL.

Pros: truly canonical manifest, no derived state in the DB, computed in one place, survives SERVER_URL changes. Fixes screenshots for free.

Cons: small extra wiring per asset field; computed on every fetch (negligible).

Option C — Make the public-assets endpoint registration-scoped

  • Change the route from /public-assets/:workspaceId/:applicationId/*path to /public-assets/:applicationRegistrationId/:universalIdentifier/*path so URLs no longer require an install context.
  • Bake those URLs into the registration manifest at deploy/sync time (uniformly across NPM / dev / tarball).

Pros: URLs become install-independent → can live in the manifest cleanly. findMarketplaceAppDetail doesn't need workspaceId. Pre-install marketplace browse can show real logos too.

Cons: FileFolder.PublicAsset storage is currently keyed by (workspaceId, universalIdentifier). Sharing assets across workspaces installing the same registration requires changing the storage key, plus a migration. Loses workspace-scoped isolation of public assets (encryption, retention, GDPR) — possibly an explicit non-goal.

Recommendation

Option B. It's the only one that keeps applicationRegistration.manifest truly canonical and gets the install boundary in the right place. Option A is the smaller win-now if B feels too much for one PR — both are interchangeable. Option C is a valid long-term direction (real "install-agnostic" public assets) but is a separate, larger conversation about asset isolation across tenants.

Out of scope for this issue

  • The dev path also bakes workspace-specific URLs into the shared registration manifest. It works today only because dev = single workspace per registration. Whichever option above is picked should also clean that up so the dev and tarball paths converge.
  • The same problem applies to manifest screenshots[]. Option B handles this for free; option A would need a sibling column.

Where this came up

PR #20181 (twenty-linear app, generic OAuth provider). The user installed twenty-linear@0.1.3 via tarball deploy, the SVG asset was correctly uploaded to FileFolder.PublicAsset, but the app's About page renders the default beige L placeholder instead of the Linear logo because manifest.logoUrl = \"public/linear-logomark.svg\" is loaded verbatim by the front.

Assessmentadvisory
bug●●● hard85% confidence

Registration-scoped manifest URLs are incompatible with install-scoped public assets, requiring architectural changes to asset resolution across sync, dev, and tarball deploy paths.

Likely files
  • packages/twenty-server/src/engine/core-modules/applications/services/marketplace-catalog-sync.service.ts
  • packages/twenty-server/src/engine/core-modules/applications/resolvers/application-development.resolver.ts
  • packages/twenty-server/src/engine/core-modules/applications/services/application-tarball.service.ts
  • packages/twenty-server/src/engine/core-modules/applications/services/application-registration.service.ts
  • packages/twenty-server/src/engine/core-modules/file/services/file.service.ts
  • packages/twenty-server/src/engine/core-modules/applications/dtos/application-manifest.dto.ts
  • packages/twenty-front/src/modules/app-manifest/hooks/useResolveAppAssetUrl.ts
Create the request

This opens a fresh agent run and a draft PR for issue #20203.

Cancel