Showing preview only (2,616K chars total). Download the full file or copy to clipboard to get everything.
Repository: usemarble/marble
Branch: main
Commit: 711d2595ae00
Files: 730
Total size: 2.3 MB
Directory structure:
gitextract_4dlc520n/
├── .cursor/
│ └── rules/
│ ├── mintlify.mdc
│ └── ultracite.mdc
├── .dockerignore
├── .github/
│ ├── CONTRIBUTING.md
│ ├── FUNDING.yml
│ ├── ISSUE_TEMPLATE/
│ │ ├── bug_report.md
│ │ └── feature_request.md
│ ├── pull_request_template.md
│ └── workflows/
│ ├── code-quality.yml
│ └── tests.yml
├── .gitignore
├── .husky/
│ └── commit-msg
├── .npmrc
├── .vscode/
│ ├── extensions.json
│ └── settings.json
├── CODE_OF_CONDUCT.md
├── LICENSE
├── README.md
├── apps/
│ ├── api/
│ │ ├── .gitignore
│ │ ├── README.md
│ │ ├── package.json
│ │ ├── src/
│ │ │ ├── app.ts
│ │ │ ├── index.ts
│ │ │ ├── lib/
│ │ │ │ ├── cache.ts
│ │ │ │ ├── constants.ts
│ │ │ │ ├── crypto.ts
│ │ │ │ ├── db.ts
│ │ │ │ ├── events.ts
│ │ │ │ ├── media.ts
│ │ │ │ ├── polar.ts
│ │ │ │ ├── posts.ts
│ │ │ │ ├── redis.ts
│ │ │ │ ├── sanitize.ts
│ │ │ │ ├── usage.ts
│ │ │ │ └── workspace.ts
│ │ │ ├── middleware/
│ │ │ │ ├── analytics.ts
│ │ │ │ ├── authorization.ts
│ │ │ │ ├── cache.ts
│ │ │ │ ├── key-authorization.ts
│ │ │ │ ├── legacy-analytics.ts
│ │ │ │ ├── ratelimit.ts
│ │ │ │ └── system.ts
│ │ │ ├── routes/
│ │ │ │ ├── authors.ts
│ │ │ │ ├── cache.ts
│ │ │ │ ├── categories.ts
│ │ │ │ ├── events.ts
│ │ │ │ ├── invalidate.ts
│ │ │ │ ├── media.ts
│ │ │ │ ├── posts.ts
│ │ │ │ └── tags.ts
│ │ │ ├── schemas/
│ │ │ │ ├── authors.ts
│ │ │ │ ├── categories.ts
│ │ │ │ ├── common.ts
│ │ │ │ ├── media.ts
│ │ │ │ ├── posts.ts
│ │ │ │ └── tags.ts
│ │ │ ├── types/
│ │ │ │ └── env.ts
│ │ │ └── validations/
│ │ │ ├── authors.ts
│ │ │ ├── categories.ts
│ │ │ ├── json.ts
│ │ │ ├── misc.ts
│ │ │ ├── posts.ts
│ │ │ └── tags.ts
│ │ ├── tsconfig.json
│ │ └── wrangler.jsonc
│ ├── cms/
│ │ ├── .env.example
│ │ ├── .gitignore
│ │ ├── README.md
│ │ ├── components.json
│ │ ├── next.config.ts
│ │ ├── package.json
│ │ ├── postcss.config.mjs
│ │ ├── public/
│ │ │ └── manifest.json
│ │ ├── src/
│ │ │ ├── app/
│ │ │ │ ├── (auth)/
│ │ │ │ │ ├── join/
│ │ │ │ │ │ └── [id]/
│ │ │ │ │ │ ├── page-client.tsx
│ │ │ │ │ │ └── page.tsx
│ │ │ │ │ ├── layout.tsx
│ │ │ │ │ ├── login/
│ │ │ │ │ │ └── page.tsx
│ │ │ │ │ ├── new/
│ │ │ │ │ │ ├── page-client.tsx
│ │ │ │ │ │ └── page.tsx
│ │ │ │ │ ├── register/
│ │ │ │ │ │ └── page.tsx
│ │ │ │ │ ├── reset/
│ │ │ │ │ │ └── page.tsx
│ │ │ │ │ └── verify/
│ │ │ │ │ └── page.tsx
│ │ │ │ ├── (main)/
│ │ │ │ │ ├── [workspace]/
│ │ │ │ │ │ ├── (dashboard)/
│ │ │ │ │ │ │ ├── (home)/
│ │ │ │ │ │ │ │ ├── page-client.tsx
│ │ │ │ │ │ │ │ └── page.tsx
│ │ │ │ │ │ │ ├── authors/
│ │ │ │ │ │ │ │ ├── page-client.tsx
│ │ │ │ │ │ │ │ └── page.tsx
│ │ │ │ │ │ │ ├── categories/
│ │ │ │ │ │ │ │ ├── page-client.tsx
│ │ │ │ │ │ │ │ └── page.tsx
│ │ │ │ │ │ │ ├── layout.tsx
│ │ │ │ │ │ │ ├── loading.tsx
│ │ │ │ │ │ │ ├── media/
│ │ │ │ │ │ │ │ ├── [id]/
│ │ │ │ │ │ │ │ │ ├── page-client.tsx
│ │ │ │ │ │ │ │ │ └── page.tsx
│ │ │ │ │ │ │ │ ├── page-client.tsx
│ │ │ │ │ │ │ │ └── page.tsx
│ │ │ │ │ │ │ ├── posts/
│ │ │ │ │ │ │ │ ├── page-client.tsx
│ │ │ │ │ │ │ │ └── page.tsx
│ │ │ │ │ │ │ ├── settings/
│ │ │ │ │ │ │ │ ├── account/
│ │ │ │ │ │ │ │ │ ├── page-client.tsx
│ │ │ │ │ │ │ │ │ └── page.tsx
│ │ │ │ │ │ │ │ ├── appearance/
│ │ │ │ │ │ │ │ │ ├── page-client.tsx
│ │ │ │ │ │ │ │ │ └── page.tsx
│ │ │ │ │ │ │ │ ├── billing/
│ │ │ │ │ │ │ │ │ ├── page-client.tsx
│ │ │ │ │ │ │ │ │ └── page.tsx
│ │ │ │ │ │ │ │ ├── fields/
│ │ │ │ │ │ │ │ │ ├── page-client.tsx
│ │ │ │ │ │ │ │ │ └── page.tsx
│ │ │ │ │ │ │ │ ├── general/
│ │ │ │ │ │ │ │ │ ├── page-client.tsx
│ │ │ │ │ │ │ │ │ └── page.tsx
│ │ │ │ │ │ │ │ ├── keys/
│ │ │ │ │ │ │ │ │ ├── page-client.tsx
│ │ │ │ │ │ │ │ │ └── page.tsx
│ │ │ │ │ │ │ │ ├── members/
│ │ │ │ │ │ │ │ │ ├── page-client.tsx
│ │ │ │ │ │ │ │ │ └── page.tsx
│ │ │ │ │ │ │ │ ├── notifications/
│ │ │ │ │ │ │ │ │ ├── page-client.tsx
│ │ │ │ │ │ │ │ │ └── page.tsx
│ │ │ │ │ │ │ │ └── webhooks/
│ │ │ │ │ │ │ │ ├── page-client.tsx
│ │ │ │ │ │ │ │ └── page.tsx
│ │ │ │ │ │ │ └── tags/
│ │ │ │ │ │ │ ├── page-client.tsx
│ │ │ │ │ │ │ └── page.tsx
│ │ │ │ │ │ ├── (editor)/
│ │ │ │ │ │ │ ├── editor/
│ │ │ │ │ │ │ │ └── p/
│ │ │ │ │ │ │ │ ├── [id]/
│ │ │ │ │ │ │ │ │ ├── page-client.tsx
│ │ │ │ │ │ │ │ │ └── page.tsx
│ │ │ │ │ │ │ │ └── new/
│ │ │ │ │ │ │ │ ├── page-client.tsx
│ │ │ │ │ │ │ │ └── page.tsx
│ │ │ │ │ │ │ └── layout.tsx
│ │ │ │ │ │ ├── layout.tsx
│ │ │ │ │ │ ├── loading.tsx
│ │ │ │ │ │ └── set-workspace-cookie.tsx
│ │ │ │ │ └── layout.tsx
│ │ │ │ ├── (share)/
│ │ │ │ │ ├── layout.tsx
│ │ │ │ │ └── share/
│ │ │ │ │ └── [token]/
│ │ │ │ │ ├── page-client.tsx
│ │ │ │ │ └── page.tsx
│ │ │ │ ├── api/
│ │ │ │ │ ├── accounts/
│ │ │ │ │ │ ├── [id]/
│ │ │ │ │ │ │ └── route.ts
│ │ │ │ │ │ └── route.ts
│ │ │ │ │ ├── ai/
│ │ │ │ │ │ └── suggestions/
│ │ │ │ │ │ ├── prompt.ts
│ │ │ │ │ │ └── route.tsx
│ │ │ │ │ ├── auth/
│ │ │ │ │ │ └── [...all]/
│ │ │ │ │ │ └── route.ts
│ │ │ │ │ ├── authors/
│ │ │ │ │ │ ├── [id]/
│ │ │ │ │ │ │ └── route.ts
│ │ │ │ │ │ └── route.ts
│ │ │ │ │ ├── categories/
│ │ │ │ │ │ ├── [id]/
│ │ │ │ │ │ │ └── route.ts
│ │ │ │ │ │ └── route.ts
│ │ │ │ │ ├── fields/
│ │ │ │ │ │ ├── [id]/
│ │ │ │ │ │ │ └── route.ts
│ │ │ │ │ │ └── route.ts
│ │ │ │ │ ├── keys/
│ │ │ │ │ │ ├── [id]/
│ │ │ │ │ │ │ └── route.ts
│ │ │ │ │ │ └── route.ts
│ │ │ │ │ ├── media/
│ │ │ │ │ │ ├── [id]/
│ │ │ │ │ │ │ └── route.ts
│ │ │ │ │ │ ├── editor/
│ │ │ │ │ │ │ └── route.ts
│ │ │ │ │ │ └── route.ts
│ │ │ │ │ ├── metrics/
│ │ │ │ │ │ ├── publishing/
│ │ │ │ │ │ │ └── route.ts
│ │ │ │ │ │ └── usage/
│ │ │ │ │ │ └── route.ts
│ │ │ │ │ ├── polar/
│ │ │ │ │ │ └── success/
│ │ │ │ │ │ └── route.ts
│ │ │ │ │ ├── posts/
│ │ │ │ │ │ ├── [id]/
│ │ │ │ │ │ │ ├── fields/
│ │ │ │ │ │ │ │ └── route.ts
│ │ │ │ │ │ │ └── route.ts
│ │ │ │ │ │ ├── import/
│ │ │ │ │ │ │ └── route.ts
│ │ │ │ │ │ └── route.ts
│ │ │ │ │ ├── share/
│ │ │ │ │ │ ├── [token]/
│ │ │ │ │ │ │ └── route.ts
│ │ │ │ │ │ └── route.ts
│ │ │ │ │ ├── tags/
│ │ │ │ │ │ ├── [id]/
│ │ │ │ │ │ │ └── route.ts
│ │ │ │ │ │ └── route.ts
│ │ │ │ │ ├── upload/
│ │ │ │ │ │ ├── complete/
│ │ │ │ │ │ │ └── route.ts
│ │ │ │ │ │ └── route.ts
│ │ │ │ │ ├── user/
│ │ │ │ │ │ ├── notifications/
│ │ │ │ │ │ │ └── route.ts
│ │ │ │ │ │ └── route.ts
│ │ │ │ │ ├── webhooks/
│ │ │ │ │ │ ├── [id]/
│ │ │ │ │ │ │ ├── route.ts
│ │ │ │ │ │ │ └── test/
│ │ │ │ │ │ │ └── route.ts
│ │ │ │ │ │ └── route.ts
│ │ │ │ │ └── workspaces/
│ │ │ │ │ ├── [slug]/
│ │ │ │ │ │ └── route.ts
│ │ │ │ │ └── route.ts
│ │ │ │ ├── layout.tsx
│ │ │ │ ├── not-found.tsx
│ │ │ │ ├── providers.tsx
│ │ │ │ └── robots.ts
│ │ │ ├── components/
│ │ │ │ ├── auth/
│ │ │ │ │ ├── login-form.tsx
│ │ │ │ │ ├── register-form.tsx
│ │ │ │ │ ├── reset/
│ │ │ │ │ │ ├── reset-form.tsx
│ │ │ │ │ │ └── reset-request-form.tsx
│ │ │ │ │ └── verify-form.tsx
│ │ │ │ ├── authors/
│ │ │ │ │ ├── author-modals.tsx
│ │ │ │ │ ├── author-sheet.tsx
│ │ │ │ │ ├── columns.tsx
│ │ │ │ │ ├── data-table.tsx
│ │ │ │ │ └── table-actions.tsx
│ │ │ │ ├── billing/
│ │ │ │ │ ├── success-modal.tsx
│ │ │ │ │ └── upgrade-modal.tsx
│ │ │ │ ├── categories/
│ │ │ │ │ ├── category-modals.tsx
│ │ │ │ │ ├── columns.tsx
│ │ │ │ │ ├── data-table.tsx
│ │ │ │ │ └── table-actions.tsx
│ │ │ │ ├── editor/
│ │ │ │ │ ├── ai/
│ │ │ │ │ │ └── readability-suggestions.tsx
│ │ │ │ │ ├── editor-data-provider.tsx
│ │ │ │ │ ├── editor-header.tsx
│ │ │ │ │ ├── editor-page.tsx
│ │ │ │ │ ├── editor-sidebar.tsx
│ │ │ │ │ ├── editor.tsx
│ │ │ │ │ ├── fields/
│ │ │ │ │ │ ├── author-selector.tsx
│ │ │ │ │ │ ├── category-selector.tsx
│ │ │ │ │ │ ├── cover-image-selector.tsx
│ │ │ │ │ │ ├── custom-fields-section.tsx
│ │ │ │ │ │ ├── description-field.tsx
│ │ │ │ │ │ ├── featured-field.tsx
│ │ │ │ │ │ ├── field-info.tsx
│ │ │ │ │ │ ├── publish-date-field.tsx
│ │ │ │ │ │ ├── slug-field.tsx
│ │ │ │ │ │ ├── status-field.tsx
│ │ │ │ │ │ └── tag-selector.tsx
│ │ │ │ │ ├── footer/
│ │ │ │ │ │ └── metadata-footer.tsx
│ │ │ │ │ ├── link-selector.tsx
│ │ │ │ │ ├── share-modal.tsx
│ │ │ │ │ ├── tabs/
│ │ │ │ │ │ ├── analysis-tab.tsx
│ │ │ │ │ │ └── metadata-tab.tsx
│ │ │ │ │ └── textarea-autosize.tsx
│ │ │ │ ├── fields/
│ │ │ │ │ ├── create-custom-field.tsx
│ │ │ │ │ ├── custom-field-row.tsx
│ │ │ │ │ ├── delete-custom-field.tsx
│ │ │ │ │ ├── edit-custom-field.tsx
│ │ │ │ │ └── field-options-input.tsx
│ │ │ │ ├── home/
│ │ │ │ │ ├── api-usage-card.tsx
│ │ │ │ │ ├── media-usage-card.tsx
│ │ │ │ │ ├── publishing-activity-card.tsx
│ │ │ │ │ └── webhook-usage-card.tsx
│ │ │ │ ├── icons/
│ │ │ │ │ ├── marble.tsx
│ │ │ │ │ └── social/
│ │ │ │ │ └── index.tsx
│ │ │ │ ├── invoice/
│ │ │ │ │ ├── columns.tsx
│ │ │ │ │ ├── data-table.tsx
│ │ │ │ │ └── table-actions.tsx
│ │ │ │ ├── keys/
│ │ │ │ │ ├── api-key-modal.tsx
│ │ │ │ │ ├── columns.tsx
│ │ │ │ │ ├── data-table.tsx
│ │ │ │ │ ├── delete-key.tsx
│ │ │ │ │ └── table-actions.tsx
│ │ │ │ ├── layout/
│ │ │ │ │ ├── header-sidebar-trigger.tsx
│ │ │ │ │ ├── page-header.tsx
│ │ │ │ │ └── wrapper.tsx
│ │ │ │ ├── media/
│ │ │ │ │ ├── crop-image-modal.tsx
│ │ │ │ │ ├── delete-modal.tsx
│ │ │ │ │ ├── file-upload-input.tsx
│ │ │ │ │ ├── media-actions.tsx
│ │ │ │ │ ├── media-card.tsx
│ │ │ │ │ ├── media-columns.tsx
│ │ │ │ │ ├── media-controls.tsx
│ │ │ │ │ ├── media-data-table.tsx
│ │ │ │ │ ├── media-gallery.tsx
│ │ │ │ │ ├── media-table-toolbar.tsx
│ │ │ │ │ ├── upload-modal.tsx
│ │ │ │ │ └── video-player.tsx
│ │ │ │ ├── nav/
│ │ │ │ │ ├── announcements.tsx
│ │ │ │ │ ├── app-breadcrumb.tsx
│ │ │ │ │ ├── app-sidebar.tsx
│ │ │ │ │ ├── create-workspace-dialog.tsx
│ │ │ │ │ ├── nav-extra.tsx
│ │ │ │ │ ├── nav-main.tsx
│ │ │ │ │ ├── nav-settings.tsx
│ │ │ │ │ ├── nav-user.tsx
│ │ │ │ │ ├── sidebar-footer-content.tsx
│ │ │ │ │ ├── theme-toggle.tsx
│ │ │ │ │ ├── upgrade-card.tsx
│ │ │ │ │ ├── whats-new-card.tsx
│ │ │ │ │ └── workspace-switcher.tsx
│ │ │ │ ├── posts/
│ │ │ │ │ ├── columns.tsx
│ │ │ │ │ ├── data-grid.tsx
│ │ │ │ │ ├── data-table.tsx
│ │ │ │ │ ├── data-view.tsx
│ │ │ │ │ ├── import-item-form.tsx
│ │ │ │ │ ├── import-modal.tsx
│ │ │ │ │ ├── post-actions.tsx
│ │ │ │ │ └── post-modals.tsx
│ │ │ │ ├── settings/
│ │ │ │ │ ├── account.tsx
│ │ │ │ │ ├── delete-account.tsx
│ │ │ │ │ ├── fields/
│ │ │ │ │ │ ├── delete.tsx
│ │ │ │ │ │ ├── id.tsx
│ │ │ │ │ │ ├── logo.tsx
│ │ │ │ │ │ ├── name.tsx
│ │ │ │ │ │ ├── slug.tsx
│ │ │ │ │ │ └── timezone.tsx
│ │ │ │ │ ├── section.tsx
│ │ │ │ │ └── theme.tsx
│ │ │ │ ├── share/
│ │ │ │ │ ├── prose.tsx
│ │ │ │ │ └── screens.tsx
│ │ │ │ ├── shared/
│ │ │ │ │ ├── container.tsx
│ │ │ │ │ ├── dropzone.tsx
│ │ │ │ │ ├── icons.tsx
│ │ │ │ │ ├── page-loader.tsx
│ │ │ │ │ └── pending-state.tsx
│ │ │ │ ├── tags/
│ │ │ │ │ ├── columns.tsx
│ │ │ │ │ ├── data-table.tsx
│ │ │ │ │ ├── table-actions.tsx
│ │ │ │ │ └── tag-modals.tsx
│ │ │ │ ├── team/
│ │ │ │ │ ├── columns.tsx
│ │ │ │ │ ├── data-table.tsx
│ │ │ │ │ ├── invite-button.tsx
│ │ │ │ │ ├── invite-modal.tsx
│ │ │ │ │ ├── invite-section.tsx
│ │ │ │ │ ├── leave-workspace.tsx
│ │ │ │ │ ├── profile-sheet.tsx
│ │ │ │ │ ├── table-actions.tsx
│ │ │ │ │ └── team-modals.tsx
│ │ │ │ ├── ui/
│ │ │ │ │ ├── async-button.tsx
│ │ │ │ │ ├── copy-button.tsx
│ │ │ │ │ ├── data-table-pagination.tsx
│ │ │ │ │ ├── error-message.tsx
│ │ │ │ │ ├── gauge.tsx
│ │ │ │ │ ├── hidden-scrollbar.tsx
│ │ │ │ │ ├── last-used-badge.tsx
│ │ │ │ │ ├── loading-spinner.tsx
│ │ │ │ │ ├── segmented-progress.tsx
│ │ │ │ │ └── timezone-selector.tsx
│ │ │ │ └── webhooks/
│ │ │ │ ├── create-webhook.tsx
│ │ │ │ ├── delete-webhook.tsx
│ │ │ │ ├── edit-webhook.tsx
│ │ │ │ ├── webhook-actions.tsx
│ │ │ │ ├── webhook-card.tsx
│ │ │ │ ├── webhook-columns.tsx
│ │ │ │ └── webhook-data-table.tsx
│ │ │ ├── hooks/
│ │ │ │ ├── use-debounce.ts
│ │ │ │ ├── use-isomorphic-layout-effect.ts
│ │ │ │ ├── use-localstorage.ts
│ │ │ │ ├── use-media-actions.ts
│ │ │ │ ├── use-media-query.ts
│ │ │ │ ├── use-mobile.tsx
│ │ │ │ ├── use-plan.ts
│ │ │ │ ├── use-readability.ts
│ │ │ │ └── use-workspace-id.ts
│ │ │ ├── lib/
│ │ │ │ ├── actions/
│ │ │ │ │ ├── checks.ts
│ │ │ │ │ ├── email.ts
│ │ │ │ │ ├── user.ts
│ │ │ │ │ └── workspace.ts
│ │ │ │ ├── ai/
│ │ │ │ │ └── readability.ts
│ │ │ │ ├── auth/
│ │ │ │ │ ├── access.ts
│ │ │ │ │ ├── client.ts
│ │ │ │ │ ├── redirect.ts
│ │ │ │ │ ├── server.ts
│ │ │ │ │ ├── session.ts
│ │ │ │ │ ├── types.ts
│ │ │ │ │ └── workspace.ts
│ │ │ │ ├── blurhash.ts
│ │ │ │ ├── cache/
│ │ │ │ │ └── invalidate.ts
│ │ │ │ ├── cache.ts
│ │ │ │ ├── constants.ts
│ │ │ │ ├── custom-fields.ts
│ │ │ │ ├── data/
│ │ │ │ │ └── post.ts
│ │ │ │ ├── events/
│ │ │ │ │ └── dispatch.ts
│ │ │ │ ├── media/
│ │ │ │ │ └── upload.ts
│ │ │ │ ├── notifications.ts
│ │ │ │ ├── plans.ts
│ │ │ │ ├── polar/
│ │ │ │ │ ├── client.ts
│ │ │ │ │ ├── customer.created.ts
│ │ │ │ │ ├── subscription.canceled.ts
│ │ │ │ │ ├── subscription.created.ts
│ │ │ │ │ ├── subscription.revoked.ts
│ │ │ │ │ ├── subscription.updated.ts
│ │ │ │ │ └── utils.ts
│ │ │ │ ├── queries/
│ │ │ │ │ ├── keys.ts
│ │ │ │ │ ├── user.ts
│ │ │ │ │ └── workspace.ts
│ │ │ │ ├── r2.ts
│ │ │ │ ├── ratelimit.ts
│ │ │ │ ├── redis.ts
│ │ │ │ ├── search-params.ts
│ │ │ │ └── validations/
│ │ │ │ ├── auth.ts
│ │ │ │ ├── authors.ts
│ │ │ │ ├── editor.ts
│ │ │ │ ├── fields.ts
│ │ │ │ ├── keys.ts
│ │ │ │ ├── post.ts
│ │ │ │ ├── settings.ts
│ │ │ │ ├── tags.ts
│ │ │ │ ├── upload.ts
│ │ │ │ ├── webhook.ts
│ │ │ │ └── workspace.ts
│ │ │ ├── providers/
│ │ │ │ ├── user.tsx
│ │ │ │ └── workspace.tsx
│ │ │ ├── proxy.ts
│ │ │ ├── styles/
│ │ │ │ ├── editor.css
│ │ │ │ └── globals.css
│ │ │ ├── types/
│ │ │ │ ├── author.ts
│ │ │ │ ├── dashboard.ts
│ │ │ │ ├── fields.ts
│ │ │ │ ├── icons.ts
│ │ │ │ ├── media.ts
│ │ │ │ ├── misc.ts
│ │ │ │ ├── share.ts
│ │ │ │ ├── user.ts
│ │ │ │ ├── webhook.ts
│ │ │ │ └── workspace.ts
│ │ │ └── utils/
│ │ │ ├── author.tsx
│ │ │ ├── editor.ts
│ │ │ ├── fetch/
│ │ │ │ └── client.ts
│ │ │ ├── keys.ts
│ │ │ ├── media.ts
│ │ │ ├── readability.ts
│ │ │ ├── site.ts
│ │ │ ├── string.ts
│ │ │ ├── usage/
│ │ │ │ └── media.ts
│ │ │ └── workspace/
│ │ │ ├── client.ts
│ │ │ ├── constants.ts
│ │ │ └── server.ts
│ │ └── tsconfig.json
│ ├── jobs/
│ │ ├── .gitignore
│ │ ├── README.md
│ │ ├── package.json
│ │ ├── src/
│ │ │ ├── consumers/
│ │ │ │ ├── deliveries.ts
│ │ │ │ ├── dlq.ts
│ │ │ │ └── events.ts
│ │ │ ├── index.ts
│ │ │ ├── lib/
│ │ │ │ ├── db.ts
│ │ │ │ ├── formats.ts
│ │ │ │ ├── signing.ts
│ │ │ │ └── usage.ts
│ │ │ ├── scheduled/
│ │ │ │ └── cleanup.ts
│ │ │ └── types/
│ │ │ └── env.ts
│ │ ├── tsconfig.json
│ │ └── wrangler.jsonc
│ ├── mcp/
│ │ ├── .gitignore
│ │ ├── README.md
│ │ ├── package.json
│ │ ├── public/
│ │ │ ├── home.js
│ │ │ └── styles.css
│ │ ├── src/
│ │ │ ├── components/
│ │ │ │ ├── icons.tsx
│ │ │ │ └── mcp-clients.tsx
│ │ │ ├── index.ts
│ │ │ ├── lib/
│ │ │ │ ├── api.ts
│ │ │ │ ├── auth.ts
│ │ │ │ ├── constants.ts
│ │ │ │ ├── instructions.ts
│ │ │ │ ├── mcp.ts
│ │ │ │ └── media.ts
│ │ │ ├── routes/
│ │ │ │ ├── home.tsx
│ │ │ │ └── mcp.ts
│ │ │ ├── server.ts
│ │ │ ├── tools/
│ │ │ │ ├── authors.ts
│ │ │ │ ├── categories.ts
│ │ │ │ ├── media.ts
│ │ │ │ ├── posts.ts
│ │ │ │ ├── shared.ts
│ │ │ │ └── tags.ts
│ │ │ └── types.ts
│ │ ├── tsconfig.json
│ │ └── wrangler.jsonc
│ └── web/
│ ├── .gitignore
│ ├── .vscode/
│ │ └── launch.json
│ ├── README.md
│ ├── astro.config.mjs
│ ├── package.json
│ ├── public/
│ │ ├── robots.txt
│ │ └── site.webmanifest
│ ├── src/
│ │ ├── components/
│ │ │ ├── BlogHeader.astro
│ │ │ ├── CategoryCard.astro
│ │ │ ├── CategoryFilter.astro
│ │ │ ├── ChangelogCard.astro
│ │ │ ├── Container.astro
│ │ │ ├── Footer.astro
│ │ │ ├── Head.astro
│ │ │ ├── Header.astro
│ │ │ ├── PostCard.astro
│ │ │ ├── PricingCard.astro
│ │ │ ├── Prose.astro
│ │ │ ├── ScrollToTop.astro
│ │ │ ├── Welcome.astro
│ │ │ ├── icons/
│ │ │ │ ├── Collab.astro
│ │ │ │ ├── Discord.astro
│ │ │ │ ├── Github.astro
│ │ │ │ ├── Logo.astro
│ │ │ │ ├── Media.astro
│ │ │ │ ├── WordMark.astro
│ │ │ │ ├── WordMarkAlt.astro
│ │ │ │ ├── X.astro
│ │ │ │ ├── brand/
│ │ │ │ │ ├── Bounty.astro
│ │ │ │ │ ├── Candle.astro
│ │ │ │ │ ├── Databuddy.astro
│ │ │ │ │ ├── Helix.astro
│ │ │ │ │ ├── Ia.astro
│ │ │ │ │ ├── Mantlz.astro
│ │ │ │ │ └── Opencut.astro
│ │ │ │ └── sponsors/
│ │ │ │ ├── Neon.astro
│ │ │ │ ├── Upstash.astro
│ │ │ │ └── Vercel.astro
│ │ │ ├── sections/
│ │ │ │ └── Pricing.astro
│ │ │ └── ui/
│ │ │ ├── AccordionItem.astro
│ │ │ └── Button.astro
│ │ ├── content/
│ │ │ └── pages/
│ │ │ ├── privacy.md
│ │ │ └── terms.md
│ │ ├── content.config.ts
│ │ ├── layouts/
│ │ │ ├── BlogLayout.astro
│ │ │ └── Layout.astro
│ │ ├── lib/
│ │ │ ├── accordion.ts
│ │ │ ├── constants/
│ │ │ │ ├── faqs.ts
│ │ │ │ ├── landing.ts
│ │ │ │ ├── navigation.ts
│ │ │ │ ├── site.ts
│ │ │ │ └── tracking.ts
│ │ │ ├── marble.ts
│ │ │ ├── schemas.ts
│ │ │ ├── seo.ts
│ │ │ ├── site.ts
│ │ │ └── utils.ts
│ │ ├── pages/
│ │ │ ├── 404.astro
│ │ │ ├── blog/
│ │ │ │ ├── [slug].astro
│ │ │ │ ├── category/
│ │ │ │ │ └── [slug].astro
│ │ │ │ └── index.astro
│ │ │ ├── changelog/
│ │ │ │ ├── [slug].astro
│ │ │ │ └── index.astro
│ │ │ ├── contributors/
│ │ │ │ └── index.astro
│ │ │ ├── index.astro
│ │ │ ├── pricing/
│ │ │ │ └── index.astro
│ │ │ ├── privacy/
│ │ │ │ └── index.astro
│ │ │ ├── rss.xml.ts
│ │ │ ├── sponsors/
│ │ │ │ └── index.astro
│ │ │ └── terms/
│ │ │ └── index.astro
│ │ └── styles/
│ │ └── globals.css
│ ├── tailwind.config.ts
│ └── tsconfig.json
├── biome.jsonc
├── commitlint.config.ts
├── docker-compose.yml
├── package.json
├── packages/
│ ├── db/
│ │ ├── .gitignore
│ │ ├── package.json
│ │ ├── prisma/
│ │ │ ├── migrations/
│ │ │ │ ├── 0_init/
│ │ │ │ │ └── migration.sql
│ │ │ │ ├── 20250831193214_add_author_table/
│ │ │ │ │ └── migration.sql
│ │ │ │ ├── 20250907120320_make_new_primary_author_required/
│ │ │ │ │ └── migration.sql
│ │ │ │ ├── 20250907125704_drop_legacy_user_author_fields/
│ │ │ │ │ └── migration.sql
│ │ │ │ ├── 20250907194746_rename_author_fields_to_final_names/
│ │ │ │ │ └── migration.sql
│ │ │ │ ├── 20250908090455_make_primary_author_optional/
│ │ │ │ │ └── migration.sql
│ │ │ │ ├── 20250909162749_make_published_at_optional/
│ │ │ │ │ └── migration.sql
│ │ │ │ ├── 20250909171017_make_published_at_required/
│ │ │ │ │ └── migration.sql
│ │ │ │ ├── 20250911083948_add_slack_payload_format/
│ │ │ │ │ └── migration.sql
│ │ │ │ ├── 20250915114755_add_database_indices/
│ │ │ │ │ └── migration.sql
│ │ │ │ ├── 20250919210238_add_share_link_table/
│ │ │ │ │ └── migration.sql
│ │ │ │ ├── 20250923212858_add_ai_editor_preferences/
│ │ │ │ │ └── migration.sql
│ │ │ │ ├── 20250924180405_add_missing_better_auth_indices/
│ │ │ │ │ └── migration.sql
│ │ │ │ ├── 20250927161627_add_author_social_links/
│ │ │ │ │ └── migration.sql
│ │ │ │ ├── 20251114225009_add_usage_event_table/
│ │ │ │ │ └── migration.sql
│ │ │ │ ├── 20251116173412_new_media_enum_and_alt_text_column/
│ │ │ │ │ └── migration.sql
│ │ │ │ ├── 20251201001521_add_api_keys/
│ │ │ │ │ └── migration.sql
│ │ │ │ ├── 20251210213108_subscription_history/
│ │ │ │ │ └── migration.sql
│ │ │ │ ├── 20260331143009_add_fields/
│ │ │ │ │ └── migration.sql
│ │ │ │ ├── 20260505135201_add_media_metadata/
│ │ │ │ │ └── migration.sql
│ │ │ │ ├── 20260508223056_add_notification_preferences/
│ │ │ │ │ └── migration.sql
│ │ │ │ ├── 20260511_rename_webhook_to_webhook_endpoint/
│ │ │ │ │ └── migration.sql
│ │ │ │ ├── 20260513192507_add_workspace_events/
│ │ │ │ │ └── migration.sql
│ │ │ │ ├── 20260515000100_add_subscription_polar_event_ordering/
│ │ │ │ │ └── migration.sql
│ │ │ │ └── migration_lock.toml
│ │ │ └── schema.prisma
│ │ ├── prisma.config.ts
│ │ ├── src/
│ │ │ ├── browser.ts
│ │ │ ├── client.ts
│ │ │ ├── hyperdrive.ts
│ │ │ ├── index.ts
│ │ │ └── workers.ts
│ │ └── tsconfig.json
│ ├── demo-markdown.md
│ ├── editor/
│ │ ├── package.json
│ │ ├── src/
│ │ │ ├── components/
│ │ │ │ ├── color-picker.tsx
│ │ │ │ ├── editor-character-count.tsx
│ │ │ │ ├── editor-content.tsx
│ │ │ │ ├── editor-provider.tsx
│ │ │ │ ├── editor-table-menus.tsx
│ │ │ │ ├── icons/
│ │ │ │ │ ├── twitter.tsx
│ │ │ │ │ └── youtube.tsx
│ │ │ │ ├── index.ts
│ │ │ │ ├── marks/
│ │ │ │ │ ├── editor-clear-formatting.tsx
│ │ │ │ │ ├── editor-link-selector.tsx
│ │ │ │ │ ├── editor-mark-bold.tsx
│ │ │ │ │ ├── editor-mark-code.tsx
│ │ │ │ │ ├── editor-mark-highlight.tsx
│ │ │ │ │ ├── editor-mark-italic.tsx
│ │ │ │ │ ├── editor-mark-strike.tsx
│ │ │ │ │ ├── editor-mark-subscript.tsx
│ │ │ │ │ ├── editor-mark-superscript.tsx
│ │ │ │ │ ├── editor-mark-text-color.tsx
│ │ │ │ │ ├── editor-mark-underline.tsx
│ │ │ │ │ └── index.ts
│ │ │ │ ├── menus/
│ │ │ │ │ ├── block-handle-menu.tsx
│ │ │ │ │ ├── bubble-menu.tsx
│ │ │ │ │ ├── floating-menu.tsx
│ │ │ │ │ └── index.ts
│ │ │ │ ├── nodes/
│ │ │ │ │ ├── editor-align-selector.tsx
│ │ │ │ │ ├── editor-align.tsx
│ │ │ │ │ ├── editor-node-bullet-list.tsx
│ │ │ │ │ ├── editor-node-code.tsx
│ │ │ │ │ ├── editor-node-heading1.tsx
│ │ │ │ │ ├── editor-node-heading2.tsx
│ │ │ │ │ ├── editor-node-heading3.tsx
│ │ │ │ │ ├── editor-node-ordered-list.tsx
│ │ │ │ │ ├── editor-node-quote.tsx
│ │ │ │ │ ├── editor-node-table.tsx
│ │ │ │ │ ├── editor-node-task-list.tsx
│ │ │ │ │ ├── editor-node-text.tsx
│ │ │ │ │ └── index.ts
│ │ │ │ ├── rich-text-field.tsx
│ │ │ │ └── ui/
│ │ │ │ ├── editor-button.tsx
│ │ │ │ ├── editor-selector.tsx
│ │ │ │ └── index.ts
│ │ │ ├── extensions/
│ │ │ │ ├── code-block/
│ │ │ │ │ ├── code-block-comp.tsx
│ │ │ │ │ ├── code-block-view.tsx
│ │ │ │ │ ├── code-block.ts
│ │ │ │ │ └── index.ts
│ │ │ │ ├── extension-kit.ts
│ │ │ │ ├── figure/
│ │ │ │ │ ├── figure-view.tsx
│ │ │ │ │ └── index.ts
│ │ │ │ ├── image-upload/
│ │ │ │ │ ├── hooks.ts
│ │ │ │ │ ├── image-upload-comp.tsx
│ │ │ │ │ ├── image-upload-view.tsx
│ │ │ │ │ └── index.ts
│ │ │ │ ├── index.ts
│ │ │ │ ├── markdown-input/
│ │ │ │ │ ├── index.ts
│ │ │ │ │ └── utils.ts
│ │ │ │ ├── slash-command/
│ │ │ │ │ ├── groups.ts
│ │ │ │ │ ├── index.ts
│ │ │ │ │ ├── menu-list.tsx
│ │ │ │ │ └── slash-command.ts
│ │ │ │ ├── table/
│ │ │ │ │ ├── index.ts
│ │ │ │ │ ├── menus/
│ │ │ │ │ │ ├── table-column/
│ │ │ │ │ │ │ ├── index.tsx
│ │ │ │ │ │ │ └── utils.ts
│ │ │ │ │ │ └── table-row/
│ │ │ │ │ │ ├── index.tsx
│ │ │ │ │ │ └── utils.ts
│ │ │ │ │ ├── table-cell.ts
│ │ │ │ │ ├── table-header.ts
│ │ │ │ │ ├── table-row.ts
│ │ │ │ │ ├── table.ts
│ │ │ │ │ └── utils.ts
│ │ │ │ ├── twitter/
│ │ │ │ │ ├── index.tsx
│ │ │ │ │ ├── twitter-comp.tsx
│ │ │ │ │ ├── twitter-upload.ts
│ │ │ │ │ └── twitter-view.tsx
│ │ │ │ ├── video/
│ │ │ │ │ ├── index.ts
│ │ │ │ │ └── video-view.tsx
│ │ │ │ ├── video-upload/
│ │ │ │ │ ├── hooks.ts
│ │ │ │ │ ├── index.ts
│ │ │ │ │ ├── video-upload-comp.tsx
│ │ │ │ │ └── video-upload-view.tsx
│ │ │ │ └── youtube/
│ │ │ │ ├── youtube-comp.tsx
│ │ │ │ ├── youtube-upload.ts
│ │ │ │ └── youtube-view.tsx
│ │ │ ├── index.ts
│ │ │ ├── lib/
│ │ │ │ ├── index.ts
│ │ │ │ ├── lowlight.ts
│ │ │ │ └── utils.ts
│ │ │ ├── styles/
│ │ │ │ ├── color-picker.css
│ │ │ │ ├── table.css
│ │ │ │ └── task-list.css
│ │ │ └── types/
│ │ │ └── index.ts
│ │ └── tsconfig.json
│ ├── email/
│ │ ├── package.json
│ │ ├── src/
│ │ │ ├── components/
│ │ │ │ ├── button.tsx
│ │ │ │ └── footer.tsx
│ │ │ ├── emails/
│ │ │ │ ├── founder.tsx
│ │ │ │ ├── invite.tsx
│ │ │ │ ├── reset.tsx
│ │ │ │ ├── usage-limit.tsx
│ │ │ │ ├── verify.tsx
│ │ │ │ └── welcome.tsx
│ │ │ ├── index.ts
│ │ │ └── lib/
│ │ │ ├── config.ts
│ │ │ ├── dev.ts
│ │ │ └── send.ts
│ │ └── tsconfig.json
│ ├── events/
│ │ ├── package.json
│ │ ├── src/
│ │ │ ├── index.ts
│ │ │ ├── types.ts
│ │ │ └── utils/
│ │ │ ├── demo.ts
│ │ │ ├── envelope.ts
│ │ │ ├── events.ts
│ │ │ └── resources.ts
│ │ └── tsconfig.json
│ ├── parser/
│ │ ├── .gitignore
│ │ ├── package.json
│ │ ├── src/
│ │ │ └── tiptap.ts
│ │ ├── tests/
│ │ │ └── tiptap-parser.test.ts
│ │ └── tsconfig.json
│ ├── tsconfig/
│ │ ├── base.json
│ │ ├── nextjs.json
│ │ ├── package.json
│ │ └── react-library.json
│ ├── ui/
│ │ ├── README.md
│ │ ├── components.json
│ │ ├── package.json
│ │ ├── postcss.config.mjs
│ │ ├── src/
│ │ │ ├── components/
│ │ │ │ ├── alert-dialog.tsx
│ │ │ │ ├── avatar.tsx
│ │ │ │ ├── badge.tsx
│ │ │ │ ├── breadcrumb.tsx
│ │ │ │ ├── button.tsx
│ │ │ │ ├── calendar.tsx
│ │ │ │ ├── card.tsx
│ │ │ │ ├── chart.tsx
│ │ │ │ ├── checkbox.tsx
│ │ │ │ ├── collapsible.tsx
│ │ │ │ ├── command.tsx
│ │ │ │ ├── dialog.tsx
│ │ │ │ ├── drawer.tsx
│ │ │ │ ├── dropdown-menu.tsx
│ │ │ │ ├── input-otp.tsx
│ │ │ │ ├── input.tsx
│ │ │ │ ├── kibo-ui/
│ │ │ │ │ ├── contribution-graph/
│ │ │ │ │ │ └── index.tsx
│ │ │ │ │ └── image-crop/
│ │ │ │ │ └── index.tsx
│ │ │ │ ├── label.tsx
│ │ │ │ ├── pagination.tsx
│ │ │ │ ├── popover.tsx
│ │ │ │ ├── progress.tsx
│ │ │ │ ├── radio-group.tsx
│ │ │ │ ├── scroll-area.tsx
│ │ │ │ ├── select.tsx
│ │ │ │ ├── separator.tsx
│ │ │ │ ├── sheet.tsx
│ │ │ │ ├── sidebar.tsx
│ │ │ │ ├── skeleton.tsx
│ │ │ │ ├── sonner.tsx
│ │ │ │ ├── switch.tsx
│ │ │ │ ├── table.tsx
│ │ │ │ ├── tabs.tsx
│ │ │ │ ├── textarea.tsx
│ │ │ │ ├── toggle-group.tsx
│ │ │ │ ├── toggle.tsx
│ │ │ │ └── tooltip.tsx
│ │ │ ├── hooks/
│ │ │ │ └── use-mobile.ts
│ │ │ ├── lib/
│ │ │ │ └── utils.ts
│ │ │ └── styles/
│ │ │ └── globals.css
│ │ └── tsconfig.json
│ └── utils/
│ ├── package.json
│ ├── src/
│ │ ├── constants/
│ │ │ ├── api-key.ts
│ │ │ ├── plans.ts
│ │ │ ├── pricing.ts
│ │ │ └── site.ts
│ │ ├── functions/
│ │ │ ├── api-key.ts
│ │ │ ├── highlight.ts
│ │ │ └── webhooks.ts
│ │ ├── index.ts
│ │ └── types/
│ │ └── api-key.ts
│ └── tsconfig.json
├── pnpm-workspace.yaml
├── skills-lock.json
├── tsconfig.json
└── turbo.json
================================================
FILE CONTENTS
================================================
================================================
FILE: .cursor/rules/mintlify.mdc
================================================
---
alwaysApply: true
---
# Mintlify technical writing rule
You are an AI writing assistant specialized in creating exceptional technical documentation using Mintlify components and following industry-leading technical writing practices.
## Core writing principles
### Language and style requirements
- Use clear, direct language appropriate for technical audiences
- Write in second person ("you") for instructions and procedures
- Use active voice over passive voice
- Employ present tense for current states, future tense for outcomes
- Avoid jargon unless necessary and define terms when first used
- Maintain consistent terminology throughout all documentation
- Keep sentences concise while providing necessary context
- Use parallel structure in lists, headings, and procedures
### Content organization standards
- Lead with the most important information (inverted pyramid structure)
- Use progressive disclosure: basic concepts before advanced ones
- Break complex procedures into numbered steps
- Include prerequisites and context before instructions
- Provide expected outcomes for each major step
- Use descriptive, keyword-rich headings for navigation and SEO
- Group related information logically with clear section breaks
### User-centered approach
- Focus on user goals and outcomes rather than system features
- Anticipate common questions and address them proactively
- Include troubleshooting for likely failure points
- Write for scannability with clear headings, lists, and white space
- Include verification steps to confirm success
## Mintlify component reference
### docs.json
- Refer to the [docs.json schema](https://mintlify.com/docs.json) when building the docs.json file and site navigation
### Callout components
#### Note - Additional helpful information
<Note>
Supplementary information that supports the main content without interrupting flow
</Note>
#### Tip - Best practices and pro tips
<Tip>
Expert advice, shortcuts, or best practices that enhance user success
</Tip>
#### Warning - Important cautions
<Warning>
Critical information about potential issues, breaking changes, or destructive actions
</Warning>
#### Info - Neutral contextual information
<Info>
Background information, context, or neutral announcements
</Info>
#### Check - Success confirmations
<Check>
Positive confirmations, successful completions, or achievement indicators
</Check>
### Code components
#### Single code block
Example of a single code block:
```javascript config.js
const apiConfig = {
baseURL: 'https://api.example.com',
timeout: 5000,
headers: {
'Authorization': `Bearer ${process.env.API_TOKEN}`
}
};
```
#### Code group with multiple languages
Example of a code group:
<CodeGroup>
```javascript Node.js
const response = await fetch('/api/endpoint', {
headers: { Authorization: `Bearer ${apiKey}` }
});
```
```python Python
import requests
response = requests.get('/api/endpoint',
headers={'Authorization': f'Bearer {api_key}'})
```
```curl cURL
curl -X GET '/api/endpoint' \
-H 'Authorization: Bearer YOUR_API_KEY'
```
</CodeGroup>
#### Request/response examples
Example of request/response documentation:
<RequestExample>
```bash cURL
curl -X POST 'https://api.example.com/users' \
-H 'Content-Type: application/json' \
-d '{"name": "John Doe", "email": "john@example.com"}'
```
</RequestExample>
<ResponseExample>
```json Success
{
"id": "user_123",
"name": "John Doe",
"email": "john@example.com",
"created_at": "2024-01-15T10:30:00Z"
}
```
</ResponseExample>
### Structural components
#### Steps for procedures
Example of step-by-step instructions:
<Steps>
<Step title="Install dependencies">
Run `npm install` to install required packages.
<Check>
Verify installation by running `npm list`.
</Check>
</Step>
<Step title="Configure environment">
Create a `.env` file with your API credentials.
```bash
API_KEY=your_api_key_here
```
<Warning>
Never commit API keys to version control.
</Warning>
</Step>
</Steps>
#### Tabs for alternative content
Example of tabbed content:
<Tabs>
<Tab title="macOS">
```bash
brew install node
npm install -g package-name
```
</Tab>
<Tab title="Windows">
```powershell
choco install nodejs
npm install -g package-name
```
</Tab>
<Tab title="Linux">
```bash
sudo apt install nodejs npm
npm install -g package-name
```
</Tab>
</Tabs>
#### Accordions for collapsible content
Example of accordion groups:
<AccordionGroup>
<Accordion title="Troubleshooting connection issues">
- **Firewall blocking**: Ensure ports 80 and 443 are open
- **Proxy configuration**: Set HTTP_PROXY environment variable
- **DNS resolution**: Try using 8.8.8.8 as DNS server
</Accordion>
<Accordion title="Advanced configuration">
```javascript
const config = {
performance: { cache: true, timeout: 30000 },
security: { encryption: 'AES-256' }
};
```
</Accordion>
</AccordionGroup>
### Cards and columns for emphasizing information
Example of cards and card groups:
<Card title="Getting started guide" icon="rocket" href="/quickstart">
Complete walkthrough from installation to your first API call in under 10 minutes.
</Card>
<CardGroup cols={2}>
<Card title="Authentication" icon="key" href="/auth">
Learn how to authenticate requests using API keys or JWT tokens.
</Card>
<Card title="Rate limiting" icon="clock" href="/rate-limits">
Understand rate limits and best practices for high-volume usage.
</Card>
</CardGroup>
### API documentation components
#### Parameter fields
Example of parameter documentation:
<ParamField path="user_id" type="string" required>
Unique identifier for the user. Must be a valid UUID v4 format.
</ParamField>
<ParamField body="email" type="string" required>
User's email address. Must be valid and unique within the system.
</ParamField>
<ParamField query="limit" type="integer" default="10">
Maximum number of results to return. Range: 1-100.
</ParamField>
<ParamField header="Authorization" type="string" required>
Bearer token for API authentication. Format: `Bearer YOUR_API_KEY`
</ParamField>
#### Response fields
Example of response field documentation:
<ResponseField name="user_id" type="string" required>
Unique identifier assigned to the newly created user.
</ResponseField>
<ResponseField name="created_at" type="timestamp">
ISO 8601 formatted timestamp of when the user was created.
</ResponseField>
<ResponseField name="permissions" type="array">
List of permission strings assigned to this user.
</ResponseField>
#### Expandable nested fields
Example of nested field documentation:
<ResponseField name="user" type="object">
Complete user object with all associated data.
<Expandable title="User properties">
<ResponseField name="profile" type="object">
User profile information including personal details.
<Expandable title="Profile details">
<ResponseField name="first_name" type="string">
User's first name as entered during registration.
</ResponseField>
<ResponseField name="avatar_url" type="string | null">
URL to user's profile picture. Returns null if no avatar is set.
</ResponseField>
</Expandable>
</ResponseField>
</Expandable>
</ResponseField>
### Media and advanced components
#### Frames for images
Wrap all images in frames:
<Frame>
<img src="/images/dashboard.png" alt="Main dashboard showing analytics overview" />
</Frame>
<Frame caption="The analytics dashboard provides real-time insights">
<img src="/images/analytics.png" alt="Analytics dashboard with charts" />
</Frame>
#### Videos
Use the HTML video element for self-hosted video content:
<video
controls
className="w-full aspect-video rounded-xl"
src="link-to-your-video.com"
></video>
Embed YouTube videos using iframe elements:
<iframe
className="w-full aspect-video rounded-xl"
src="https://www.youtube.com/embed/4KzFe50RQkQ"
title="YouTube video player"
frameBorder="0"
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
allowFullScreen
></iframe>
#### Tooltips
Example of tooltip usage:
<Tooltip tip="Application Programming Interface - protocols for building software">
API
</Tooltip>
#### Updates
Use updates for changelogs:
<Update label="Version 2.1.0" description="Released March 15, 2024">
## New features
- Added bulk user import feature
- Improved error messages with actionable suggestions
## Bug fixes
- Fixed pagination issue with large datasets
- Resolved authentication timeout problems
</Update>
## Required page structure
Every documentation page must begin with YAML frontmatter:
```yaml
---
title: "Clear, specific, keyword-rich title"
description: "Concise description explaining page purpose and value"
---
```
## Content quality standards
### Code examples requirements
- Always include complete, runnable examples that users can copy and execute
- Show proper error handling and edge case management
- Use realistic data instead of placeholder values
- Include expected outputs and results for verification
- Test all code examples thoroughly before publishing
- Specify language and include filename when relevant
- Add explanatory comments for complex logic
- Never include real API keys or secrets in code examples
### API documentation requirements
- Document all parameters including optional ones with clear descriptions
- Show both success and error response examples with realistic data
- Include rate limiting information with specific limits
- Provide authentication examples showing proper format
- Explain all HTTP status codes and error handling
- Cover complete request/response cycles
### Accessibility requirements
- Include descriptive alt text for all images and diagrams
- Use specific, actionable link text instead of "click here"
- Ensure proper heading hierarchy starting with H2
- Provide keyboard navigation considerations
- Use sufficient color contrast in examples and visuals
- Structure content for easy scanning with headers and lists
## Component selection logic
- Use **Steps** for procedures and sequential instructions
- Use **Tabs** for platform-specific content or alternative approaches
- Use **CodeGroup** when showing the same concept in multiple programming languages
- Use **Accordions** for progressive disclosure of information
- Use **RequestExample/ResponseExample** specifically for API endpoint documentation
- Use **ParamField** for API parameters, **ResponseField** for API responses
- Use **Expandable** for nested object properties or hierarchical information
================================================
FILE: .cursor/rules/ultracite.mdc
================================================
---
description: Ultracite Rules - AI-Ready Formatter and Linter
globs: "**/*.{ts,tsx,js,jsx}"
alwaysApply: true
---
# Project Context
Ultracite enforces strict type safety, accessibility standards, and consistent code quality for JavaScript/TypeScript projects using Biome's lightning-fast formatter and linter.
## Key Principles
- Zero configuration required
- Subsecond performance
- Maximum type safety
- AI-friendly code generation
- Always use pnpm as the package manager
## Before Writing Code
1. Analyze existing patterns in the codebase
2. Consider edge cases and error scenarios
3. Follow the rules below strictly
4. Validate accessibility requirements
## Rules
### Accessibility (a11y)
- Don't use `accessKey` attribute on any HTML element.
- Don't set `aria-hidden="true"` on focusable elements.
- Don't add ARIA roles, states, and properties to elements that don't support them.
- Don't use distracting elements like `<marquee>` or `<blink>`.
- Only use the `scope` prop on `<th>` elements.
- Don't assign non-interactive ARIA roles to interactive HTML elements.
- Make sure label elements have text content and are associated with an input.
- Don't assign interactive ARIA roles to non-interactive HTML elements.
- Don't assign `tabIndex` to non-interactive HTML elements.
- Don't use positive integers for `tabIndex` property.
- Don't include "image", "picture", or "photo" in img alt prop.
- Don't use explicit role property that's the same as the implicit/default role.
- Make static elements with click handlers use a valid role attribute.
- Always include a `title` element for SVG elements.
- Give all elements requiring alt text meaningful information for screen readers.
- Make sure anchors have content that's accessible to screen readers.
- Assign `tabIndex` to non-interactive HTML elements with `aria-activedescendant`.
- Include all required ARIA attributes for elements with ARIA roles.
- Make sure ARIA properties are valid for the element's supported roles.
- Always include a `type` attribute for button elements.
- Make elements with interactive roles and handlers focusable.
- Give heading elements content that's accessible to screen readers (not hidden with `aria-hidden`).
- Always include a `lang` attribute on the html element.
- Always include a `title` attribute for iframe elements.
- Accompany `onClick` with at least one of: `onKeyUp`, `onKeyDown`, or `onKeyPress`.
- Accompany `onMouseOver`/`onMouseOut` with `onFocus`/`onBlur`.
- Include caption tracks for audio and video elements.
- Use semantic elements instead of role attributes in JSX.
- Make sure all anchors are valid and navigable.
- Ensure all ARIA properties (`aria-*`) are valid.
- Use valid, non-abstract ARIA roles for elements with ARIA roles.
- Use valid ARIA state and property values.
- Use valid values for the `autocomplete` attribute on input elements.
- Use correct ISO language/country codes for the `lang` attribute.
### Code Complexity and Quality
- Don't use consecutive spaces in regular expression literals.
- Don't use the `arguments` object.
- Don't use primitive type aliases or misleading types.
- Don't use the comma operator.
- Don't use empty type parameters in type aliases and interfaces.
- Don't write functions that exceed a given Cognitive Complexity score.
- Don't nest describe() blocks too deeply in test files.
- Don't use unnecessary boolean casts.
- Don't use unnecessary callbacks with flatMap.
- Use for...of statements instead of Array.forEach.
- Don't create classes that only have static members (like a static namespace).
- Don't use this and super in static contexts.
- Don't use unnecessary catch clauses.
- Don't use unnecessary constructors.
- Don't use unnecessary continue statements.
- Don't export empty modules that don't change anything.
- Don't use unnecessary escape sequences in regular expression literals.
- Don't use unnecessary fragments.
- Don't use unnecessary labels.
- Don't use unnecessary nested block statements.
- Don't rename imports, exports, and destructured assignments to the same name.
- Don't use unnecessary string or template literal concatenation.
- Don't use String.raw in template literals when there are no escape sequences.
- Don't use useless case statements in switch statements.
- Don't use ternary operators when simpler alternatives exist.
- Don't use useless `this` aliasing.
- Don't use any or unknown as type constraints.
- Don't initialize variables to undefined.
- Don't use the void operators (they're not familiar).
- Use arrow functions instead of function expressions.
- Use Date.now() to get milliseconds since the Unix Epoch.
- Use .flatMap() instead of map().flat() when possible.
- Use literal property access instead of computed property access.
- Don't use parseInt() or Number.parseInt() when binary, octal, or hexadecimal literals work.
- Use concise optional chaining instead of chained logical expressions.
- Use regular expression literals instead of the RegExp constructor when possible.
- Don't use number literal object member names that aren't base 10 or use underscore separators.
- Remove redundant terms from logical expressions.
- Use while loops instead of for loops when you don't need initializer and update expressions.
- Don't pass children as props.
- Don't reassign const variables.
- Don't use constant expressions in conditions.
- Don't use `Math.min` and `Math.max` to clamp values when the result is constant.
- Don't return a value from a constructor.
- Don't use empty character classes in regular expression literals.
- Don't use empty destructuring patterns.
- Don't call global object properties as functions.
- Don't declare functions and vars that are accessible outside their block.
- Make sure builtins are correctly instantiated.
- Don't use super() incorrectly inside classes. Also check that super() is called in classes that extend other constructors.
- Don't use variables and function parameters before they're declared.
- Don't use 8 and 9 escape sequences in string literals.
- Don't use literal numbers that lose precision.
### React and JSX Best Practices
- Don't use the return value of React.render.
- Make sure all dependencies are correctly specified in React hooks.
- Make sure all React hooks are called from the top level of component functions.
- Don't forget key props in iterators and collection literals.
- Don't destructure props inside JSX components in Solid projects.
- Don't define React components inside other components.
- Don't use event handlers on non-interactive elements.
- Don't assign to React component props.
- Don't use both `children` and `dangerouslySetInnerHTML` props on the same element.
- Don't use dangerous JSX props.
- Don't use Array index in keys.
- Don't insert comments as text nodes.
- Don't assign JSX properties multiple times.
- Don't add extra closing tags for components without children.
- Use `<>...</>` instead of `<Fragment>...</Fragment>`.
- Watch out for possible "wrong" semicolons inside JSX elements.
### Correctness and Safety
- Don't assign a value to itself.
- Don't return a value from a setter.
- Don't compare expressions that modify string case with non-compliant values.
- Don't use lexical declarations in switch clauses.
- Don't use variables that haven't been declared in the document.
- Don't write unreachable code.
- Make sure super() is called exactly once on every code path in a class constructor before this is accessed if the class has a superclass.
- Don't use control flow statements in finally blocks.
- Don't use optional chaining where undefined values aren't allowed.
- Don't have unused function parameters.
- Don't have unused imports.
- Don't have unused labels.
- Don't have unused private class members.
- Don't have unused variables.
- Make sure void (self-closing) elements don't have children.
- Don't return a value from a function with the return type 'void'
- Use isNaN() when checking for NaN.
- Make sure "for" loop update clauses move the counter in the right direction.
- Make sure typeof expressions are compared to valid values.
- Make sure generator functions contain yield.
- Don't use await inside loops.
- Don't use bitwise operators.
- Don't use expressions where the operation doesn't change the value.
- Make sure Promise-like statements are handled appropriately.
- Don't use __dirname and __filename in the global scope.
- Prevent import cycles.
- Don't use configured elements.
- Don't hardcode sensitive data like API keys and tokens.
- Don't let variable declarations shadow variables from outer scopes.
- Don't use the TypeScript directive @ts-ignore.
- Prevent duplicate polyfills from Polyfill.io.
- Don't use useless backreferences in regular expressions that always match empty strings.
- Don't use unnecessary escapes in string literals.
- Don't use useless undefined.
- Make sure getters and setters for the same property are next to each other in class and object definitions.
- Make sure object literals are declared consistently (defaults to explicit definitions).
- Use static Response methods instead of new Response() constructor when possible.
- Make sure switch-case statements are exhaustive.
- Make sure the `preconnect` attribute is used when using Google Fonts.
- Use `Array#{indexOf,lastIndexOf}()` instead of `Array#{findIndex,findLastIndex}()` when looking for the index of an item.
- Make sure iterable callbacks return consistent values.
- Use `with { type: "json" }` for JSON module imports.
- Use numeric separators in numeric literals.
- Use object spread instead of `Object.assign()` when constructing new objects.
- Always use the radix argument when using `parseInt()`.
- Make sure JSDoc comment lines start with a single asterisk, except for the first one.
- Include a description parameter for `Symbol()`.
- Don't use spread (`...`) syntax on accumulators.
- Don't use the `delete` operator.
- Don't access namespace imports dynamically.
- Don't use namespace imports.
- Declare regex literals at the top level.
- Don't use `target="_blank"` without `rel="noopener"`.
### TypeScript Best Practices
- Don't use TypeScript enums.
- Don't export imported variables.
- Don't add type annotations to variables, parameters, and class properties that are initialized with literal expressions.
- Don't use TypeScript namespaces.
- Don't use non-null assertions with the `!` postfix operator.
- Don't use parameter properties in class constructors.
- Don't use user-defined types.
- Use `as const` instead of literal types and type annotations.
- Use either `T[]` or `Array<T>` consistently.
- Initialize each enum member value explicitly.
- Use `export type` for types.
- Use `import type` for types.
- Make sure all enum members are literal values.
- Don't use TypeScript const enum.
- Don't declare empty interfaces.
- Don't let variables evolve into any type through reassignments.
- Don't use the any type.
- Don't misuse the non-null assertion operator (!) in TypeScript files.
- Don't use implicit any type on variable declarations.
- Don't merge interfaces and classes unsafely.
- Don't use overload signatures that aren't next to each other.
- Use the namespace keyword instead of the module keyword to declare TypeScript namespaces.
### Style and Consistency
- Don't use global `eval()`.
- Don't use callbacks in asynchronous tests and hooks.
- Don't use negation in `if` statements that have `else` clauses.
- Don't use nested ternary expressions.
- Don't reassign function parameters.
- This rule lets you specify global variable names you don't want to use in your application.
- Don't use specified modules when loaded by import or require.
- Don't use constants whose value is the upper-case version of their name.
- Use `String.slice()` instead of `String.substr()` and `String.substring()`.
- Don't use template literals if you don't need interpolation or special-character handling.
- Don't use `else` blocks when the `if` block breaks early.
- Don't use yoda expressions.
- Don't use Array constructors.
- Use `at()` instead of integer index access.
- Follow curly brace conventions.
- Use `else if` instead of nested `if` statements in `else` clauses.
- Use single `if` statements instead of nested `if` clauses.
- Use `new` for all builtins except `String`, `Number`, and `Boolean`.
- Use consistent accessibility modifiers on class properties and methods.
- Use `const` declarations for variables that are only assigned once.
- Put default function parameters and optional function parameters last.
- Include a `default` clause in switch statements.
- Use the `**` operator instead of `Math.pow`.
- Use `for-of` loops when you need the index to extract an item from the iterated array.
- Use `node:assert/strict` over `node:assert`.
- Use the `node:` protocol for Node.js builtin modules.
- Use Number properties instead of global ones.
- Use assignment operator shorthand where possible.
- Use function types instead of object types with call signatures.
- Use template literals over string concatenation.
- Use `new` when throwing an error.
- Don't throw non-Error values.
- Use `String.trimStart()` and `String.trimEnd()` over `String.trimLeft()` and `String.trimRight()`.
- Use standard constants instead of approximated literals.
- Don't assign values in expressions.
- Don't use async functions as Promise executors.
- Don't reassign exceptions in catch clauses.
- Don't reassign class members.
- Don't compare against -0.
- Don't use labeled statements that aren't loops.
- Don't use void type outside of generic or return types.
- Don't use console.
- Don't use control characters and escape sequences that match control characters in regular expression literals.
- Don't use debugger.
- Don't assign directly to document.cookie.
- Use `===` and `!==`.
- Don't use duplicate case labels.
- Don't use duplicate class members.
- Don't use duplicate conditions in if-else-if chains.
- Don't use two keys with the same name inside objects.
- Don't use duplicate function parameter names.
- Don't have duplicate hooks in describe blocks.
- Don't use empty block statements and static blocks.
- Don't let switch clauses fall through.
- Don't reassign function declarations.
- Don't allow assignments to native objects and read-only global variables.
- Use Number.isFinite instead of global isFinite.
- Use Number.isNaN instead of global isNaN.
- Don't assign to imported bindings.
- Don't use irregular whitespace characters.
- Don't use labels that share a name with a variable.
- Don't use characters made with multiple code points in character class syntax.
- Make sure to use new and constructor properly.
- Don't use shorthand assign when the variable appears on both sides.
- Don't use octal escape sequences in string literals.
- Don't use Object.prototype builtins directly.
- Don't redeclare variables, functions, classes, and types in the same scope.
- Don't have redundant "use strict".
- Don't compare things where both sides are exactly the same.
- Don't let identifiers shadow restricted names.
- Don't use sparse arrays (arrays with holes).
- Don't use template literal placeholder syntax in regular strings.
- Don't use the then property.
- Don't use unsafe negation.
- Don't use var.
- Don't use with statements in non-strict contexts.
- Make sure async functions actually use await.
- Make sure default clauses in switch statements come last.
- Make sure to pass a message value when creating a built-in error.
- Make sure get methods always return a value.
- Use a recommended display strategy with Google Fonts.
- Make sure for-in loops include an if statement.
- Use Array.isArray() instead of instanceof Array.
- Make sure to use the digits argument with Number#toFixed().
- Make sure to use the "use strict" directive in script files.
### Next.js Specific Rules
- Don't use `<img>` elements in Next.js projects.
- Don't use `<head>` elements in Next.js projects.
- Don't import next/document outside of pages/_document.jsx in Next.js projects.
- Don't use the next/head module in pages/_document.js on Next.js projects.
### Phosphor Icons
- Always use the Phosphor Icons package.
- Always use icon as the suffix for the icon component e.g. `UploadSimpleIcon` instead of `UploadSimple`.
### Testing Best Practices
- Don't use export or module.exports in test files.
- Don't use focused tests.
- Make sure the assertion function, like expect, is placed inside an it() function call.
- Don't use disabled tests.
## Common Tasks
- `npx ultracite init` - Initialize Ultracite in your project
- `npx ultracite fix` - Format and fix code automatically
- `npx ultracite check` - Check for issues without fixing
## Example: Error Handling
```typescript
// ✅ Good: Comprehensive error handling
try {
const result = await fetchData();
return { success: true, data: result };
} catch (error) {
console.error('API call failed:', error);
return { success: false, error: error.message };
}
// ❌ Bad: Swallowing errors
try {
return await fetchData();
} catch (e) {
console.log(e);
}
```
================================================
FILE: .dockerignore
================================================
.git
node_modules
pnpm-store
**/.next
**/.turbo
**/dist
**/.output
**/.vercel
.env
.env.*
!.env.example
!**/.env.example
coverage
coverage/**
playwright-report
playwright-report/**
*.log
*.swp
*.DS_Store
================================================
FILE: .github/CONTRIBUTING.md
================================================
# Contributing to Marble
Thanks for your interest in contributing! This guide explains how to get Marble running locally and how to submit high-quality contributions.
## Prerequisites
Before you start, make sure you have the following installed or available:
- **Node.js** ≥ 20.x
- **pnpm** ≥ 10.x (install with `npm i -g pnpm`)
- **PostgreSQL** database (we use [Neon](https://neon.tech) in examples)
- **Redis** database (we use [Upstash](https://upstash.com))
- **Google** and **GitHub** OAuth apps (for authentication)
- **Cloudflare** account with R2 enabled (for media uploads)
- **Optional**: [Polar](https://sandbox.polar.sh) sandbox account if you want to test payments
---
## Structure
This repository is a monorepo and is structured as follows:
```text
/
├── apps/
│ ├── api/ → Hono REST API
│ ├── cms/ → Next.js dashboard
│ ├── docs/ → Mintlify documentation
│ └── web/ → Astro marketing site
├── packages/
│ ├── db/ → Prisma schema + client (shared by api & cms)
│ ├── editor/ → Tiptap-based rich text editor
│ ├── email/ → Email templates
│ ├── parser/ → Content parsing utilities
│ ├── tsconfig/ → Shared TypeScript configurations
│ ├── ui/ → shadcn/ui components (shared UI library)
│ └── utils/ → Shared utilities
├── .npmrc
├── package.json
├── pnpm-workspace.yaml
├── README.md
└── turbo.json
```
### Apps
This directory contains the source code for all related applications:
- **api**: [Hono](https://hono.dev) REST API for content delivery
- **cms**: [Next.js](https://nextjs.org) app for the dashboard
- **docs**: [Mintlify](https://mintlify.com) documentation site
- **web**: [Astro](https://astro.build) app for the marketing website
### Packages
Packages contain internal shared modules used across different applications:
- **db**: Prisma schema and client shared between the `api` and `cms` apps
- **editor**: Tiptap-based rich text editor used in the CMS
- **email**: Email templates for notifications and transactional emails
- **parser**: Content parsing utilities
- **tsconfig**: TypeScript configurations shared across the monorepo
- **ui**: shadcn/ui components used in the `cms` app
- **utils**: Shared utilities used across apps and packages
## Getting Started
1. [Fork](https://github.com/usemarble/marble/fork/) this repository to your own account
- Visit Marble repository
- Click the "Fork" button in the top right
- [Clone](https://help.github.com/articles/cloning-a-repository/) the fork to your local device.
```bash
git clone https://github.com/YOUR-USERNAME/marble.git
cd marble
```
- add the original repo as upstream
```bash
git remote add upstream https://github.com/usemarble/marble.git
```
2. Install Dependencies
```bash
pnpm install
```
3. Configure Environment Variables
Each app/package that uses environment variables has an example env file. You’ll need to copy and fill those out:
```bash
cp apps/api/.dev.vars.example apps/api/.dev.vars
cp apps/cms/.env.example apps/cms/.env
cp apps/web/.env.example apps/web/.env
cp packages/db/.env.example packages/db/.env
```
You'll need:
- A Postgres connection string (either neon or use docker to self host)
- Redis credentials from Upstash (see below)
- Google and GitHub OAuth credentials (how to get these)
- A BetterAuth secret
- Cloudflare R2 credentials for file uploads (see below)
- Optional: If you want to test payments, set up a [Polar](https://sandbox.polar.sh) sandbox account and fill in the POLAR_* variables.
4. Database Setup
### Option 1: Use Neon (Hosted)
We use Neon for the database. Create a Neon project and copy your connection string for Prisma
(ensure it includes `sslmode=require`).
- Paste it into the relevant env files:
Example:
```bash
DATABASE_URL="postgresql://<user>:<password>@<host>/<db>?sslmode=require"
```
- Paste it into the relevant env files:
- `apps/api/.dev.vars` → `DATABASE_URL=<YOUR_STRING_HERE>`
- `apps/cms/.env` → `DATABASE_URL=<YOUR_STRING_HERE>`
- `packages/db/.env` → `DATABASE_URL=<YOUR_STRING_HERE>`
- Run migrations:
```bash
pnpm db:migrate
```
### Option 2: Use Docker (Local)
Prerequisites: Docker Desktop (macOS/Windows) or Docker Engine + Docker Compose v2 (Linux).
Start a local Postgres and run migrations:
```bash
# from repo root
pnpm docker:up
# wait for the DB to be healthy (one of):
# pnpm docker:logs # watch for "database system is ready to accept connections"
# docker compose ps # ensure STATUS is "healthy"
## Alternatively, if your Docker Compose version supports it:
# docker compose up -d --wait
pnpm db:migrate
```
If you’re using the local Docker DB, set `DATABASE_URL` in these env files:
- `apps/api/.dev.vars`
- `apps/cms/.env`
- `packages/db/.env`
Example:
```bash
DATABASE_URL=postgresql://usemarble:justusemarble@localhost:5432/marble
```
Note: These credentials are for local development only. Do not use them in production.
This will:
- Build (if needed) and start the Postgres container defined in `docker-compose.yml`.
- Expose Postgres on port `5432` using the credentials from the compose file.
- Persist data in the `marble_pgdata` Docker volume.
- Note: If you already have a local Postgres on port 5432, stop it or adjust the port mapping in `docker-compose.yml`.
Useful commands:
```bash
pnpm docker:logs # follow DB logs
pnpm docker:down # stop containers
pnpm docker:clean # stop and remove volumes (DESTROYS local data)
```
### Google OAuth
Create a project in the Google Cloud Console.
Follow [the first step](https://www.better-auth.com/docs/authentication/google) in the Better Auth documentation to set up Google OAuth and set the values for `GOOGLE_CLIENT_ID` and `GOOGLE_CLIENT_SECRET`.
### GitHub OAuth
If you would rather use github you can follow [the first step](https://www.better-auth.com/docs/authentication/github#get-your-github-credentials) in the better auth docs to setup Github oAuth and set the environment values for `GITHUB_ID` and `GITHUB_SECRET`
### Set up Cloudflare R2 for media uploads
To use media uploads in Marble, you’ll need to set up a Cloudflare R2 bucket. Here's a step-by-step guide to help you configure everything properly:
- Go to your Cloudflare dashboard
- Select your account and navigate to R2 from the sidebar
- Click "Create Bucket"
- Name your bucket (e.g. marble-media)
- Hit 'Create"
- switch to the settings tab and enable "public development url"
- copy the value to `CLOUDFLARE_PUBLIC_URL`
- Set your bucket name to `CLOUDFLARE_BUCKET_NAME`
- Go back to your R2 buckets overview and click "API"
- from the dropdown select "Use r2 with apis"
- then copy the api url and set to `CLOUDFLARE_S3_ENDPOINT`
- Below the url click "Create api Tokens"
- Select "Create user API Token"
- For permissions select "admin read and write"
- Leave everything else as default and click "Create user API Token"
- Copy the values to `CLOUDFLARE_SECRET_ACCESS_KEY` and `CLOUDFLARE_ACCESS_KEY_ID` respectively
## Set up Redis
### Option 1: Use Upstash Redis
Marble uses Redis for rate limiting, session caching, and analytics. We use [Upstash](https://upstash.com) for serverless Redis. Here's how to set it up:
- Go to [Upstash Console](https://console.upstash.com) and sign in or create an account
- Click "Create Database"
- Give your database a name (e.g. marble-redis)
- Select a region close to your primary deployment region
- Leave the type as "Regional" (free tier)
- Click "Create"
- Once created, scroll down to the "REST API" section
- Copy the "UPSTASH_REDIS_REST_URL" value and set it to `REDIS_URL` in your environment files
- Copy the "UPSTASH_REDIS_REST_TOKEN" value and set it to `REDIS_TOKEN` in your environment files
You'll need to add these to:
- `apps/api/.dev.vars` → `REDIS_URL=<YOUR_URL_HERE>` and `REDIS_TOKEN=<YOUR_TOKEN_HERE>`
- `apps/cms/.env` → `REDIS_URL=<YOUR_URL_HERE>` and `REDIS_TOKEN=<YOUR_TOKEN_HERE>`
### Option 2: Docker (Local)
Use the repository's Docker Compose services to run Redis locally (native Redis + Upstash-compatible HTTP bridge):
```bash
# from repo root
pnpm docker:up
pnpm docker:ps
```
Expected Redis services:
- `redis` on `localhost:6379`
- `serverless-redis-http` on `localhost:8079`
Set these in your env files:
- `apps/api/.dev.vars` → `REDIS_URL=http://localhost:8079` and `REDIS_TOKEN=justusemarble`
- `apps/cms/.env` → `REDIS_URL=http://localhost:8079` and `REDIS_TOKEN=justusemarble`
These values match the local defaults in:
- `apps/api/.dev.vars.example`
- `apps/cms/.env.example`
- `docker-compose.yml` (`SRH_TOKEN=justusemarble`, `8079:80`, `6379:6379`)
Stop services when done:
```bash
pnpm docker:down
```
## Running the Apps
From the root you can run all apps
```bash
pnpm dev
```
or just one
```bash
pnpm cms:dev
pnpm api:dev
pnpm web:dev
pnpm docs:dev
```
## Contributing to docs
The documentation lives in `apps/docs` and is built with [Mintlify](https://mintlify.com). To contribute:
### Prerequisites
- Node.js v20.17.0+ (same as the main project)
- No database, Redis, or OAuth setup required
### Option A: From the monorepo root (recommended)
After `pnpm install`, run:
```bash
pnpm docs:dev
```
This uses the Mintlify CLI from the workspace—no global install needed. Docs preview at `http://localhost:3000`.
### Option B: Manual setup
If you prefer the CLI globally:
```bash
pnpm add -g mint
cd apps/docs
mint dev
```
### Port conflicts
If port 3000 is in use (e.g. by another app), use a different port:
```bash
mint dev --port 3333
```
### Useful commands
From `apps/docs`:
- `mint broken-links` — Find broken internal links
- `mint a11y` — Check accessibility (contrast, alt text)
- `mint validate` — Validate the build (useful for CI)
### Project structure
See [apps/docs/README.md](../apps/docs/README.md) for the docs file layout.
## Agent skills (optional)
The repo root has a [`skills-lock.json`](../skills-lock.json) file that pins [Agent Skills](https://skills.sh/) (for example Cloudflare, Tiptap, Vercel labs, and Resend-related packages). Installed skill files live under gitignored paths (for example `.agents/skills/`, `.cursor/skills/`, and `.claude/skills/`) so they are not committed.
After cloning, you can restore the same skills from the lockfile from the repository root:
```bash
npx skills experimental_install
```
`experimental_install` is the current CLI command that reads `skills-lock.json`. Adding a package updates the lockfile — for example:
```bash
npx skills add https://github.com/cloudflare/skills
npx skills add ueberdosis/tiptap
npx skills add vercel-labs/agent-skills
npx skills add resend/email-best-practices
```
If you add or change skills for the team, commit the updated `skills-lock.json`.
## Making changes
1. Create a new branch for your changes
```bash
git checkout -b feature/your-feature
```
2. Before committing your changes make sure to run the lint and format commands (via ultracite/Biome) to catch any issues
```bash
pnpm format
```
or if you would rather fix issues yourself, run the following to list problems without fixing
```bash
pnpm lint
```
3. Test your changes and make sure they work and run a build
```bash
pnpm build
```
4. If your build succeeds you can go ahead and make a commit using conventional [commit messages](https://www.conventionalcommits.org/en/v1.0.0/)
```bash
git commit -m "fix(cms): fix sidebar overflow issue"
```
## Pull Request Guidelines
- Your PR should reference an issue (if applicable) or clearly describe its impact on the project. [see how to Link a pull request to an issue](https://docs.github.com/en/issues/tracking-your-work-with-issues/using-issues/linking-a-pull-request-to-an-issue)
- Include a clear description of the changes
- Keep PRs small and focused. Large PRs are harder to review and may be rejected or delayed.
- Ensure consistency with the existing codebase. Use ultracite (Biome) for linting and formatting.
- Include tests if applicable
- Update documentation if your changes affect usage or API behavior.
## Code Style
- Follow the existing code formatting in the project (use ultracite/Biome for consistency).
- Write clear, self-documenting code
- Add comments only when necessary to explain complex logic
- Use meaningful variable and function names
## Reporting Issues
- Use the GitHub issue tracker
- Check if the issue already exists before creating a new one
- Provide a clear description of the issue
- Include steps to reproduce the issue
## Need Help?
Feel free to open an issue for questions or join our [discord](https://discord.gg/gU44Pmwqkx).
Thank you for contributing!
================================================
FILE: .github/FUNDING.yml
================================================
github: [usemarble]
================================================
FILE: .github/ISSUE_TEMPLATE/bug_report.md
================================================
---
name: Bug report
about: Create a report to help us improve
title: ''
labels: ''
assignees: ''
---
**Describe the bug**
A clear and concise description of what the bug is.
**To Reproduce**
Steps to reproduce the behavior:
1. Go to '...'
2. Click on '....'
3. Scroll down to '....'
4. See error
**Expected behavior**
A clear and concise description of what you expected to happen.
**Screenshots**
If applicable, add screenshots to help explain your problem.
**Desktop (please complete the following information):**
- OS: [e.g. iOS]
- Browser [e.g. chrome, safari]
- Version [e.g. 22]
**Smartphone (please complete the following information):**
- Device: [e.g. iPhone6]
- OS: [e.g. iOS8.1]
- Browser [e.g. stock browser, safari]
- Version [e.g. 22]
**Additional context**
Add any other context about the problem here.
================================================
FILE: .github/ISSUE_TEMPLATE/feature_request.md
================================================
---
name: Feature request
about: Suggest an idea for this project
title: ''
labels: ''
assignees: ''
---
**Is your feature request related to a problem? Please describe.**
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
**Describe the solution you'd like**
A clear and concise description of what you want to happen.
**Describe alternatives you've considered**
A clear and concise description of any alternative solutions or features you've considered.
**Additional context**
Add any other context or screenshots about the feature request here.
================================================
FILE: .github/pull_request_template.md
================================================
## Description
<!--- Clearly describe what this PR changes. Include relevant details. -->
## Motivation and Context
<!--- Why is this change needed? What problem does it solve? -->
<!--- If applicable, link to related GitHub issues with `Closes #issue_number` -->
## How to Test
<!--- Provide step-by-step instructions on how to verify your changes work as expected. -->
<!--- Include any setup or test cases if needed. -->
## Screenshots (if applicable)
<!--- Add screenshots to illustrate UI changes. -->
## Video Demo (if applicable)
<!--- Show screen recordings of the issue or feature. -->
## Types of Changes
<!--- Mark all that apply with an `x` -->
- [ ] 🐛 Bug fix (non-breaking change that fixes an issue)
- [ ] ✨ New feature (non-breaking change that adds functionality)
- [ ] ⚠️ Breaking change (fix or feature that alters existing functionality)
- [ ] 🎨 UI/UX Improvements
- [ ] ⚡ Performance Enhancement
- [ ] 📖 Documentation (updates to README, docs, or comments)
================================================
FILE: .github/workflows/code-quality.yml
================================================
name: Code quality
on:
push:
pull_request:
jobs:
quality:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v5
- name: Install Node.js
uses: actions/setup-node@v4
- uses: pnpm/action-setup@v4
name: Install pnpm
- name: Install dependencies
run: pnpm install --ignore-scripts
- name: Run lint
run: pnpm lint
================================================
FILE: .github/workflows/tests.yml
================================================
name: Tests
on:
push:
pull_request:
workflow_dispatch:
jobs:
tests:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v5
- name: Install Node.js
uses: actions/setup-node@v4
- uses: pnpm/action-setup@v4
name: Install pnpm
- name: Install dependencies
run: pnpm install --ignore-scripts
- name: Run tests
run: pnpm test
================================================
FILE: .gitignore
================================================
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
# Dependencies
node_modules
.pnp
.pnp.js
# Local env files
.env
.env.local
.env.development.local
.env.test.local
.env.production.local
# Testing
coverage
# Turbo
.turbo
# Vercel
.vercel
# Cloudflare
.wrangler
# Build Outputs
.next/
out/
build
dist
packages/db/src/generated/
.idea
# Debug
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# Misc
.DS_Store
*.pem
# Agent skills (install locally via npx skills add)
.agents/skills/
.agent/skills/
.cursor/skills/
.claude/skills/
docs
# Local only references
school/
================================================
FILE: .husky/commit-msg
================================================
pnpm exec commitlint --edit "$1"
================================================
FILE: .npmrc
================================================
public-hoist-pattern[]=*prisma*
================================================
FILE: .vscode/extensions.json
================================================
{
"recommendations": [
"biomejs.biome",
"astro-build.astro-vscode",
"bradlc.vscode-tailwindcss",
"Prisma.prisma"
]
}
================================================
FILE: .vscode/settings.json
================================================
{
"tailwindCSS.experimental.configFile": {
"./apps/web/src/styles/globals.css": ["./apps/web/src/**"],
"./packages/ui/src/styles/globals.css": [
"./packages/ui/src/**",
"./packages/editor/src/**",
"./apps/cms/src/**"
]
},
"tailwindCSS.includeLanguages": {
"astro": "html"
},
"tailwindCSS.classFunctions": ["cn", "clsx"],
"files.associations": {
"*.css": "tailwindcss"
},
"tailwindCSS.experimental.classRegex": [
[
"(?:export\\s+)?const\\s+[A-Za-z0-9_$]+\\s*=\\s*([\\s\\S]*?);",
"[\"'`]([^\"'`]*)[\"'`]"
],
"return\\s+[\"'`]([^\"'`]*)[\"'`]"
],
"editor.defaultFormatter": "biomejs.biome",
"[javascript]": {
"editor.defaultFormatter": "biomejs.biome"
},
"[typescript]": {
"editor.defaultFormatter": "biomejs.biome"
},
"[typescriptreact]": {
"editor.defaultFormatter": "biomejs.biome"
},
"editor.codeActionsOnSave": {
"source.organizeImports.biome": "explicit",
"source.fixAll.biome": "explicit"
},
"editor.formatOnSave": true,
"editor.formatOnPaste": true,
"search.exclude": {
"**/node_modules": true
},
"[prisma]": {
"editor.defaultFormatter": "Prisma.prisma"
},
"[javascriptreact]": {
"editor.defaultFormatter": "biomejs.biome"
},
"[json]": {
"editor.defaultFormatter": "biomejs.biome"
},
"[jsonc]": {
"editor.defaultFormatter": "biomejs.biome"
},
"[css]": {
"editor.defaultFormatter": "biomejs.biome"
},
"[graphql]": {
"editor.defaultFormatter": "biomejs.biome"
},
"typescript.tsdk": "node_modules/typescript/lib",
"emmet.showExpandedAbbreviation": "never"
}
================================================
FILE: CODE_OF_CONDUCT.md
================================================
# Contributor Covenant Code of Conduct
## Our Pledge
We as members, contributors, and leaders pledge to make participation in our
community a harassment-free experience for everyone, regardless of age, body
size, visible or invisible disability, ethnicity, sex characteristics, gender
identity and expression, level of experience, education, socio-economic status,
nationality, personal appearance, race, religion, or sexual identity
and orientation.
We pledge to act and interact in ways that contribute to an open, welcoming,
diverse, inclusive, and healthy community.
## Our Standards
Examples of behavior that contributes to a positive environment for our
community include:
* Demonstrating empathy and kindness toward other people
* Being respectful of differing opinions, viewpoints, and experiences
* Giving and gracefully accepting constructive feedback
* Accepting responsibility and apologizing to those affected by our mistakes,
and learning from the experience
* Focusing on what is best not just for us as individuals, but for the
overall community
Examples of unacceptable behavior include:
* The use of sexualized language or imagery, and sexual attention or
advances of any kind
* Trolling, insulting or derogatory comments, and personal or political attacks
* Public or private harassment
* Publishing others' private information, such as a physical or email
address, without their explicit permission
* Other conduct which could reasonably be considered inappropriate in a
professional setting
## Enforcement Responsibilities
Community leaders are responsible for clarifying and enforcing our standards of
acceptable behavior and will take appropriate and fair corrective action in
response to any behavior that they deem inappropriate, threatening, offensive,
or harmful.
Community leaders have the right and responsibility to remove, edit, or reject
comments, commits, code, wiki edits, issues, and other contributions that are
not aligned to this Code of Conduct, and will communicate reasons for moderation
decisions when appropriate.
## Scope
This Code of Conduct applies within all community spaces, and also applies when
an individual is officially representing the community in public spaces.
Examples of representing our community include using an official e-mail address,
posting via an official social media account, or acting as an appointed
representative at an online or offline event.
## Enforcement
Instances of abusive, harassing, or otherwise unacceptable behavior may be
reported to the community leaders responsible for enforcement at
support@marblecms.com.
All complaints will be reviewed and investigated promptly and fairly.
All community leaders are obligated to respect the privacy and security of the
reporter of any incident.
## Enforcement Guidelines
Community leaders will follow these Community Impact Guidelines in determining
the consequences for any action they deem in violation of this Code of Conduct:
### 1. Correction
**Community Impact**: Use of inappropriate language or other behavior deemed
unprofessional or unwelcome in the community.
**Consequence**: A private, written warning from community leaders, providing
clarity around the nature of the violation and an explanation of why the
behavior was inappropriate. A public apology may be requested.
### 2. Warning
**Community Impact**: A violation through a single incident or series
of actions.
**Consequence**: A warning with consequences for continued behavior. No
interaction with the people involved, including unsolicited interaction with
those enforcing the Code of Conduct, for a specified period of time. This
includes avoiding interactions in community spaces as well as external channels
like social media. Violating these terms may lead to a temporary or
permanent ban.
### 3. Temporary Ban
**Community Impact**: A serious violation of community standards, including
sustained inappropriate behavior.
**Consequence**: A temporary ban from any sort of interaction or public
communication with the community for a specified period of time. No public or
private interaction with the people involved, including unsolicited interaction
with those enforcing the Code of Conduct, is allowed during this period.
Violating these terms may lead to a permanent ban.
### 4. Permanent Ban
**Community Impact**: Demonstrating a pattern of violation of community
standards, including sustained inappropriate behavior, harassment of an
individual, or aggression toward or disparagement of classes of individuals.
**Consequence**: A permanent ban from any sort of public interaction within
the community.
## Attribution
This Code of Conduct is adapted from the [Contributor Covenant][homepage],
version 2.0, available at
https://www.contributor-covenant.org/version/2/0/code_of_conduct.html.
Community Impact Guidelines were inspired by [Mozilla's code of conduct
enforcement ladder](https://github.com/mozilla/diversity).
[homepage]: https://www.contributor-covenant.org
For answers to common questions about this code of conduct, see the FAQ at
https://www.contributor-covenant.org/faq. Translations are available at
https://www.contributor-covenant.org/translations.
================================================
FILE: LICENSE
================================================
GNU AFFERO GENERAL PUBLIC LICENSE
Version 3, 19 November 2007
Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
Everyone is permitted to copy and distribute verbatim copies
of this license document, but changing it is not allowed.
Preamble
The GNU Affero General Public License is a free, copyleft license for
software and other kinds of works, specifically designed to ensure
cooperation with the community in the case of network server software.
The licenses for most software and other practical works are designed
to take away your freedom to share and change the works. By contrast,
our General Public Licenses are intended to guarantee your freedom to
share and change all versions of a program--to make sure it remains free
software for all its users.
When we speak of free software, we are referring to freedom, not
price. Our General Public Licenses are designed to make sure that you
have the freedom to distribute copies of free software (and charge for
them if you wish), that you receive source code or can get it if you
want it, that you can change the software or use pieces of it in new
free programs, and that you know you can do these things.
Developers that use our General Public Licenses protect your rights
with two steps: (1) assert copyright on the software, and (2) offer
you this License which gives you legal permission to copy, distribute
and/or modify the software.
A secondary benefit of defending all users' freedom is that
improvements made in alternate versions of the program, if they
receive widespread use, become available for other developers to
incorporate. Many developers of free software are heartened and
encouraged by the resulting cooperation. However, in the case of
software used on network servers, this result may fail to come about.
The GNU General Public License permits making a modified version and
letting the public access it on a server without ever releasing its
source code to the public.
The GNU Affero General Public License is designed specifically to
ensure that, in such cases, the modified source code becomes available
to the community. It requires the operator of a network server to
provide the source code of the modified version running there to the
users of that server. Therefore, public use of a modified version, on
a publicly accessible server, gives the public access to the source
code of the modified version.
An older license, called the Affero General Public License and
published by Affero, was designed to accomplish similar goals. This is
a different license, not a version of the Affero GPL, but Affero has
released a new version of the Affero GPL which permits relicensing under
this license.
The precise terms and conditions for copying, distribution and
modification follow.
TERMS AND CONDITIONS
0. Definitions.
"This License" refers to version 3 of the GNU Affero General Public License.
"Copyright" also means copyright-like laws that apply to other kinds of
works, such as semiconductor masks.
"The Program" refers to any copyrightable work licensed under this
License. Each licensee is addressed as "you". "Licensees" and
"recipients" may be individuals or organizations.
To "modify" a work means to copy from or adapt all or part of the work
in a fashion requiring copyright permission, other than the making of an
exact copy. The resulting work is called a "modified version" of the
earlier work or a work "based on" the earlier work.
A "covered work" means either the unmodified Program or a work based
on the Program.
To "propagate" a work means to do anything with it that, without
permission, would make you directly or secondarily liable for
infringement under applicable copyright law, except executing it on a
computer or modifying a private copy. Propagation includes copying,
distribution (with or without modification), making available to the
public, and in some countries other activities as well.
To "convey" a work means any kind of propagation that enables other
parties to make or receive copies. Mere interaction with a user through
a computer network, with no transfer of a copy, is not conveying.
An interactive user interface displays "Appropriate Legal Notices"
to the extent that it includes a convenient and prominently visible
feature that (1) displays an appropriate copyright notice, and (2)
tells the user that there is no warranty for the work (except to the
extent that warranties are provided), that licensees may convey the
work under this License, and how to view a copy of this License. If
the interface presents a list of user commands or options, such as a
menu, a prominent item in the list meets this criterion.
1. Source Code.
The "source code" for a work means the preferred form of the work
for making modifications to it. "Object code" means any non-source
form of a work.
A "Standard Interface" means an interface that either is an official
standard defined by a recognized standards body, or, in the case of
interfaces specified for a particular programming language, one that
is widely used among developers working in that language.
The "System Libraries" of an executable work include anything, other
than the work as a whole, that (a) is included in the normal form of
packaging a Major Component, but which is not part of that Major
Component, and (b) serves only to enable use of the work with that
Major Component, or to implement a Standard Interface for which an
implementation is available to the public in source code form. A
"Major Component", in this context, means a major essential component
(kernel, window system, and so on) of the specific operating system
(if any) on which the executable work runs, or a compiler used to
produce the work, or an object code interpreter used to run it.
The "Corresponding Source" for a work in object code form means all
the source code needed to generate, install, and (for an executable
work) run the object code and to modify the work, including scripts to
control those activities. However, it does not include the work's
System Libraries, or general-purpose tools or generally available free
programs which are used unmodified in performing those activities but
which are not part of the work. For example, Corresponding Source
includes interface definition files associated with source files for
the work, and the source code for shared libraries and dynamically
linked subprograms that the work is specifically designed to require,
such as by intimate data communication or control flow between those
subprograms and other parts of the work.
The Corresponding Source need not include anything that users
can regenerate automatically from other parts of the Corresponding
Source.
The Corresponding Source for a work in source code form is that
same work.
2. Basic Permissions.
All rights granted under this License are granted for the term of
copyright on the Program, and are irrevocable provided the stated
conditions are met. This License explicitly affirms your unlimited
permission to run the unmodified Program. The output from running a
covered work is covered by this License only if the output, given its
content, constitutes a covered work. This License acknowledges your
rights of fair use or other equivalent, as provided by copyright law.
You may make, run and propagate covered works that you do not
convey, without conditions so long as your license otherwise remains
in force. You may convey covered works to others for the sole purpose
of having them make modifications exclusively for you, or provide you
with facilities for running those works, provided that you comply with
the terms of this License in conveying all material for which you do
not control copyright. Those thus making or running the covered works
for you must do so exclusively on your behalf, under your direction
and control, on terms that prohibit them from making any copies of
your copyrighted material outside their relationship with you.
Conveying under any other circumstances is permitted solely under
the conditions stated below. Sublicensing is not allowed; section 10
makes it unnecessary.
3. Protecting Users' Legal Rights From Anti-Circumvention Law.
No covered work shall be deemed part of an effective technological
measure under any applicable law fulfilling obligations under article
11 of the WIPO copyright treaty adopted on 20 December 1996, or
similar laws prohibiting or restricting circumvention of such
measures.
When you convey a covered work, you waive any legal power to forbid
circumvention of technological measures to the extent such circumvention
is effected by exercising rights under this License with respect to
the covered work, and you disclaim any intention to limit operation or
modification of the work as a means of enforcing, against the work's
users, your or third parties' legal rights to forbid circumvention of
technological measures.
4. Conveying Verbatim Copies.
You may convey verbatim copies of the Program's source code as you
receive it, in any medium, provided that you conspicuously and
appropriately publish on each copy an appropriate copyright notice;
keep intact all notices stating that this License and any
non-permissive terms added in accord with section 7 apply to the code;
keep intact all notices of the absence of any warranty; and give all
recipients a copy of this License along with the Program.
You may charge any price or no price for each copy that you convey,
and you may offer support or warranty protection for a fee.
5. Conveying Modified Source Versions.
You may convey a work based on the Program, or the modifications to
produce it from the Program, in the form of source code under the
terms of section 4, provided that you also meet all of these conditions:
a) The work must carry prominent notices stating that you modified
it, and giving a relevant date.
b) The work must carry prominent notices stating that it is
released under this License and any conditions added under section
7. This requirement modifies the requirement in section 4 to
"keep intact all notices".
c) You must license the entire work, as a whole, under this
License to anyone who comes into possession of a copy. This
License will therefore apply, along with any applicable section 7
additional terms, to the whole of the work, and all its parts,
regardless of how they are packaged. This License gives no
permission to license the work in any other way, but it does not
invalidate such permission if you have separately received it.
d) If the work has interactive user interfaces, each must display
Appropriate Legal Notices; however, if the Program has interactive
interfaces that do not display Appropriate Legal Notices, your
work need not make them do so.
A compilation of a covered work with other separate and independent
works, which are not by their nature extensions of the covered work,
and which are not combined with it such as to form a larger program,
in or on a volume of a storage or distribution medium, is called an
"aggregate" if the compilation and its resulting copyright are not
used to limit the access or legal rights of the compilation's users
beyond what the individual works permit. Inclusion of a covered work
in an aggregate does not cause this License to apply to the other
parts of the aggregate.
6. Conveying Non-Source Forms.
You may convey a covered work in object code form under the terms
of sections 4 and 5, provided that you also convey the
machine-readable Corresponding Source under the terms of this License,
in one of these ways:
a) Convey the object code in, or embodied in, a physical product
(including a physical distribution medium), accompanied by the
Corresponding Source fixed on a durable physical medium
customarily used for software interchange.
b) Convey the object code in, or embodied in, a physical product
(including a physical distribution medium), accompanied by a
written offer, valid for at least three years and valid for as
long as you offer spare parts or customer support for that product
model, to give anyone who possesses the object code either (1) a
copy of the Corresponding Source for all the software in the
product that is covered by this License, on a durable physical
medium customarily used for software interchange, for a price no
more than your reasonable cost of physically performing this
conveying of source, or (2) access to copy the
Corresponding Source from a network server at no charge.
c) Convey individual copies of the object code with a copy of the
written offer to provide the Corresponding Source. This
alternative is allowed only occasionally and noncommercially, and
only if you received the object code with such an offer, in accord
with subsection 6b.
d) Convey the object code by offering access from a designated
place (gratis or for a charge), and offer equivalent access to the
Corresponding Source in the same way through the same place at no
further charge. You need not require recipients to copy the
Corresponding Source along with the object code. If the place to
copy the object code is a network server, the Corresponding Source
may be on a different server (operated by you or a third party)
that supports equivalent copying facilities, provided you maintain
clear directions next to the object code saying where to find the
Corresponding Source. Regardless of what server hosts the
Corresponding Source, you remain obligated to ensure that it is
available for as long as needed to satisfy these requirements.
e) Convey the object code using peer-to-peer transmission, provided
you inform other peers where the object code and Corresponding
Source of the work are being offered to the general public at no
charge under subsection 6d.
A separable portion of the object code, whose source code is excluded
from the Corresponding Source as a System Library, need not be
included in conveying the object code work.
A "User Product" is either (1) a "consumer product", which means any
tangible personal property which is normally used for personal, family,
or household purposes, or (2) anything designed or sold for incorporation
into a dwelling. In determining whether a product is a consumer product,
doubtful cases shall be resolved in favor of coverage. For a particular
product received by a particular user, "normally used" refers to a
typical or common use of that class of product, regardless of the status
of the particular user or of the way in which the particular user
actually uses, or expects or is expected to use, the product. A product
is a consumer product regardless of whether the product has substantial
commercial, industrial or non-consumer uses, unless such uses represent
the only significant mode of use of the product.
"Installation Information" for a User Product means any methods,
procedures, authorization keys, or other information required to install
and execute modified versions of a covered work in that User Product from
a modified version of its Corresponding Source. The information must
suffice to ensure that the continued functioning of the modified object
code is in no case prevented or interfered with solely because
modification has been made.
If you convey an object code work under this section in, or with, or
specifically for use in, a User Product, and the conveying occurs as
part of a transaction in which the right of possession and use of the
User Product is transferred to the recipient in perpetuity or for a
fixed term (regardless of how the transaction is characterized), the
Corresponding Source conveyed under this section must be accompanied
by the Installation Information. But this requirement does not apply
if neither you nor any third party retains the ability to install
modified object code on the User Product (for example, the work has
been installed in ROM).
The requirement to provide Installation Information does not include a
requirement to continue to provide support service, warranty, or updates
for a work that has been modified or installed by the recipient, or for
the User Product in which it has been modified or installed. Access to a
network may be denied when the modification itself materially and
adversely affects the operation of the network or violates the rules and
protocols for communication across the network.
Corresponding Source conveyed, and Installation Information provided,
in accord with this section must be in a format that is publicly
documented (and with an implementation available to the public in
source code form), and must require no special password or key for
unpacking, reading or copying.
7. Additional Terms.
"Additional permissions" are terms that supplement the terms of this
License by making exceptions from one or more of its conditions.
Additional permissions that are applicable to the entire Program shall
be treated as though they were included in this License, to the extent
that they are valid under applicable law. If additional permissions
apply only to part of the Program, that part may be used separately
under those permissions, but the entire Program remains governed by
this License without regard to the additional permissions.
When you convey a copy of a covered work, you may at your option
remove any additional permissions from that copy, or from any part of
it. (Additional permissions may be written to require their own
removal in certain cases when you modify the work.) You may place
additional permissions on material, added by you to a covered work,
for which you have or can give appropriate copyright permission.
Notwithstanding any other provision of this License, for material you
add to a covered work, you may (if authorized by the copyright holders of
that material) supplement the terms of this License with terms:
a) Disclaiming warranty or limiting liability differently from the
terms of sections 15 and 16 of this License; or
b) Requiring preservation of specified reasonable legal notices or
author attributions in that material or in the Appropriate Legal
Notices displayed by works containing it; or
c) Prohibiting misrepresentation of the origin of that material, or
requiring that modified versions of such material be marked in
reasonable ways as different from the original version; or
d) Limiting the use for publicity purposes of names of licensors or
authors of the material; or
e) Declining to grant rights under trademark law for use of some
trade names, trademarks, or service marks; or
f) Requiring indemnification of licensors and authors of that
material by anyone who conveys the material (or modified versions of
it) with contractual assumptions of liability to the recipient, for
any liability that these contractual assumptions directly impose on
those licensors and authors.
All other non-permissive additional terms are considered "further
restrictions" within the meaning of section 10. If the Program as you
received it, or any part of it, contains a notice stating that it is
governed by this License along with a term that is a further
restriction, you may remove that term. If a license document contains
a further restriction but permits relicensing or conveying under this
License, you may add to a covered work material governed by the terms
of that license document, provided that the further restriction does
not survive such relicensing or conveying.
If you add terms to a covered work in accord with this section, you
must place, in the relevant source files, a statement of the
additional terms that apply to those files, or a notice indicating
where to find the applicable terms.
Additional terms, permissive or non-permissive, may be stated in the
form of a separately written license, or stated as exceptions;
the above requirements apply either way.
8. Termination.
You may not propagate or modify a covered work except as expressly
provided under this License. Any attempt otherwise to propagate or
modify it is void, and will automatically terminate your rights under
this License (including any patent licenses granted under the third
paragraph of section 11).
However, if you cease all violation of this License, then your
license from a particular copyright holder is reinstated (a)
provisionally, unless and until the copyright holder explicitly and
finally terminates your license, and (b) permanently, if the copyright
holder fails to notify you of the violation by some reasonable means
prior to 60 days after the cessation.
Moreover, your license from a particular copyright holder is
reinstated permanently if the copyright holder notifies you of the
violation by some reasonable means, this is the first time you have
received notice of violation of this License (for any work) from that
copyright holder, and you cure the violation prior to 30 days after
your receipt of the notice.
Termination of your rights under this section does not terminate the
licenses of parties who have received copies or rights from you under
this License. If your rights have been terminated and not permanently
reinstated, you do not qualify to receive new licenses for the same
material under section 10.
9. Acceptance Not Required for Having Copies.
You are not required to accept this License in order to receive or
run a copy of the Program. Ancillary propagation of a covered work
occurring solely as a consequence of using peer-to-peer transmission
to receive a copy likewise does not require acceptance. However,
nothing other than this License grants you permission to propagate or
modify any covered work. These actions infringe copyright if you do
not accept this License. Therefore, by modifying or propagating a
covered work, you indicate your acceptance of this License to do so.
10. Automatic Licensing of Downstream Recipients.
Each time you convey a covered work, the recipient automatically
receives a license from the original licensors, to run, modify and
propagate that work, subject to this License. You are not responsible
for enforcing compliance by third parties with this License.
An "entity transaction" is a transaction transferring control of an
organization, or substantially all assets of one, or subdividing an
organization, or merging organizations. If propagation of a covered
work results from an entity transaction, each party to that
transaction who receives a copy of the work also receives whatever
licenses to the work the party's predecessor in interest had or could
give under the previous paragraph, plus a right to possession of the
Corresponding Source of the work from the predecessor in interest, if
the predecessor has it or can get it with reasonable efforts.
You may not impose any further restrictions on the exercise of the
rights granted or affirmed under this License. For example, you may
not impose a license fee, royalty, or other charge for exercise of
rights granted under this License, and you may not initiate litigation
(including a cross-claim or counterclaim in a lawsuit) alleging that
any patent claim is infringed by making, using, selling, offering for
sale, or importing the Program or any portion of it.
11. Patents.
A "contributor" is a copyright holder who authorizes use under this
License of the Program or a work on which the Program is based. The
work thus licensed is called the contributor's "contributor version".
A contributor's "essential patent claims" are all patent claims
owned or controlled by the contributor, whether already acquired or
hereafter acquired, that would be infringed by some manner, permitted
by this License, of making, using, or selling its contributor version,
but do not include claims that would be infringed only as a
consequence of further modification of the contributor version. For
purposes of this definition, "control" includes the right to grant
patent sublicenses in a manner consistent with the requirements of
this License.
Each contributor grants you a non-exclusive, worldwide, royalty-free
patent license under the contributor's essential patent claims, to
make, use, sell, offer for sale, import and otherwise run, modify and
propagate the contents of its contributor version.
In the following three paragraphs, a "patent license" is any express
agreement or commitment, however denominated, not to enforce a patent
(such as an express permission to practice a patent or covenant not to
sue for patent infringement). To "grant" such a patent license to a
party means to make such an agreement or commitment not to enforce a
patent against the party.
If you convey a covered work, knowingly relying on a patent license,
and the Corresponding Source of the work is not available for anyone
to copy, free of charge and under the terms of this License, through a
publicly available network server or other readily accessible means,
then you must either (1) cause the Corresponding Source to be so
available, or (2) arrange to deprive yourself of the benefit of the
patent license for this particular work, or (3) arrange, in a manner
consistent with the requirements of this License, to extend the patent
license to downstream recipients. "Knowingly relying" means you have
actual knowledge that, but for the patent license, your conveying the
covered work in a country, or your recipient's use of the covered work
in a country, would infringe one or more identifiable patents in that
country that you have reason to believe are valid.
If, pursuant to or in connection with a single transaction or
arrangement, you convey, or propagate by procuring conveyance of, a
covered work, and grant a patent license to some of the parties
receiving the covered work authorizing them to use, propagate, modify
or convey a specific copy of the covered work, then the patent license
you grant is automatically extended to all recipients of the covered
work and works based on it.
A patent license is "discriminatory" if it does not include within
the scope of its coverage, prohibits the exercise of, or is
conditioned on the non-exercise of one or more of the rights that are
specifically granted under this License. You may not convey a covered
work if you are a party to an arrangement with a third party that is
in the business of distributing software, under which you make payment
to the third party based on the extent of your activity of conveying
the work, and under which the third party grants, to any of the
parties who would receive the covered work from you, a discriminatory
patent license (a) in connection with copies of the covered work
conveyed by you (or copies made from those copies), or (b) primarily
for and in connection with specific products or compilations that
contain the covered work, unless you entered into that arrangement,
or that patent license was granted, prior to 28 March 2007.
Nothing in this License shall be construed as excluding or limiting
any implied license or other defenses to infringement that may
otherwise be available to you under applicable patent law.
12. No Surrender of Others' Freedom.
If conditions are imposed on you (whether by court order, agreement or
otherwise) that contradict the conditions of this License, they do not
excuse you from the conditions of this License. If you cannot convey a
covered work so as to satisfy simultaneously your obligations under this
License and any other pertinent obligations, then as a consequence you may
not convey it at all. For example, if you agree to terms that obligate you
to collect a royalty for further conveying from those to whom you convey
the Program, the only way you could satisfy both those terms and this
License would be to refrain entirely from conveying the Program.
13. Remote Network Interaction; Use with the GNU General Public License.
Notwithstanding any other provision of this License, if you modify the
Program, your modified version must prominently offer all users
interacting with it remotely through a computer network (if your version
supports such interaction) an opportunity to receive the Corresponding
Source of your version by providing access to the Corresponding Source
from a network server at no charge, through some standard or customary
means of facilitating copying of software. This Corresponding Source
shall include the Corresponding Source for any work covered by version 3
of the GNU General Public License that is incorporated pursuant to the
following paragraph.
Notwithstanding any other provision of this License, you have
permission to link or combine any covered work with a work licensed
under version 3 of the GNU General Public License into a single
combined work, and to convey the resulting work. The terms of this
License will continue to apply to the part which is the covered work,
but the work with which it is combined will remain governed by version
3 of the GNU General Public License.
14. Revised Versions of this License.
The Free Software Foundation may publish revised and/or new versions of
the GNU Affero General Public License from time to time. Such new versions
will be similar in spirit to the present version, but may differ in detail to
address new problems or concerns.
Each version is given a distinguishing version number. If the
Program specifies that a certain numbered version of the GNU Affero General
Public License "or any later version" applies to it, you have the
option of following the terms and conditions either of that numbered
version or of any later version published by the Free Software
Foundation. If the Program does not specify a version number of the
GNU Affero General Public License, you may choose any version ever published
by the Free Software Foundation.
If the Program specifies that a proxy can decide which future
versions of the GNU Affero General Public License can be used, that proxy's
public statement of acceptance of a version permanently authorizes you
to choose that version for the Program.
Later license versions may give you additional or different
permissions. However, no additional obligations are imposed on any
author or copyright holder as a result of your choosing to follow a
later version.
15. Disclaimer of Warranty.
THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
16. Limitation of Liability.
IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
SUCH DAMAGES.
17. Interpretation of Sections 15 and 16.
If the disclaimer of warranty and limitation of liability provided
above cannot be given local legal effect according to their terms,
reviewing courts shall apply local law that most closely approximates
an absolute waiver of all civil liability in connection with the
Program, unless a warranty or assumption of liability accompanies a
copy of the Program in return for a fee.
END OF TERMS AND CONDITIONS
How to Apply These Terms to Your New Programs
If you develop a new program, and you want it to be of the greatest
possible use to the public, the best way to achieve this is to make it
free software which everyone can redistribute and change under these terms.
To do so, attach the following notices to the program. It is safest
to attach them to the start of each source file to most effectively
state the exclusion of warranty; and each file should have at least
the "copyright" line and a pointer to where the full notice is found.
<one line to give the program's name and a brief idea of what it does.>
Copyright (C) <year> <name of author>
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published
by the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
Also add information on how to contact you by electronic and paper mail.
If your software can interact with users remotely through a computer
network, you should also make sure that it provides a way for users to
get its source. For example, if your program is a web application, its
interface could display a "Source" link that leads users to an archive
of the code. There are many ways you could offer source, and different
solutions will be better for different programs; see section 13 for the
specific requirements.
You should also get your employer (if you work as a programmer) or school,
if any, to sign a "copyright disclaimer" for the program, if necessary.
For more information on this, and how to apply and follow the GNU AGPL, see
<https://www.gnu.org/licenses/>.
================================================
FILE: README.md
================================================
<h1 align="center">Marble</h1>
<p align="center">
<a href="https://vercel.com/oss">
<img alt="Vercel OSS Program" src="https://vercel.com/oss/program-badge.svg" />
</a>
</p>
<p align="center"><em>Super simple way to publish articles, product updates and changelogs to your site</em></p>
---
## ✨ Features
- Create, edit, and manage posts in a beautiful editor
- Organise content with **tags** and **categories**
- Upload and embed **images** and **videos** in posts
- **Readability insights** and AI-powered writing suggestions in the editor
- **Realtime webhooks** to trigger workflows when content changes
- Fetch your content anywhere through the **REST API**
## 🛠 Local Development & Contributing
Want to run Marble locally or contribute a feature? Check out our
[Contributing Guide](./.github/CONTRIBUTING.md) for a step-by-step guide covering setup,
database configuration, and pull-request guidelines.
## Community & Support
Have questions or feedback?
- Join the [Discord](https://discord.gg/gU44Pmwqkx)
- Follow us on [Twitter](https://twitter.com/usemarblecms)
Feel free to open an issue for bugs or feature requests.
## License
Marble is released under the [GNU Affero General Public License v3.0](./LICENSE.md).
================================================
FILE: apps/api/.gitignore
================================================
# prod
dist/
# dev
.yarn/
!.yarn/releases
.vscode/*
!.vscode/launch.json
!.vscode/*.code-snippets
.idea/workspace.xml
.idea/usage.statistics.xml
.idea/shelf
# deps
node_modules/
.wrangler
# env
.env
.env.production
.dev.vars
# logs
logs/
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
# misc
.DS_Store
================================================
FILE: apps/api/README.md
================================================
# API
API endpoints users can fetch data from.
================================================
FILE: apps/api/package.json
================================================
{
"name": "api",
"version": "0.1.0",
"scripts": {
"dev": "wrangler dev",
"deploy": "wrangler deploy --minify",
"cf-typegen": "wrangler types --env-interface CloudflareBindings"
},
"dependencies": {
"@hono/zod-openapi": "^1.3.0",
"@hono/zod-validator": "^0.7.6",
"@marble/db": "workspace:*",
"@marble/email": "workspace:*",
"@marble/events": "workspace:*",
"@marble/utils": "workspace:*",
"@polar-sh/sdk": "^0.42.5",
"@upstash/ratelimit": "^2.0.8",
"@upstash/redis": "^1.36.4",
"hono": "^4.12.14",
"image-size": "^2.0.2",
"node-html-markdown": "^2.0.0",
"resend": "^6.12.3",
"sanitize-html": "^2.17.1",
"zod": "^4.3.6"
},
"devDependencies": {
"@cloudflare/workers-types": "^4.20260508.1",
"@types/sanitize-html": "^2.16.1",
"wrangler": "^4.90.0"
}
}
================================================
FILE: apps/api/src/app.ts
================================================
import { OpenAPIHono } from "@hono/zod-openapi";
import { Hono } from "hono";
import { cors } from "hono/cors";
import { trimTrailingSlash } from "hono/trailing-slash";
import { FRAMER_PLUGIN_PATTERN, ROUTES } from "./lib/constants";
import { analytics } from "./middleware/analytics";
import { authorization } from "./middleware/authorization";
import { cache } from "./middleware/cache";
import { keyAuthorization } from "./middleware/key-authorization";
import { legacyAnalytics } from "./middleware/legacy-analytics";
import { ratelimit } from "./middleware/ratelimit";
import { systemAuth } from "./middleware/system";
import authorsRoutes from "./routes/authors";
import cacheRoutes from "./routes/cache";
import categoriesRoutes from "./routes/categories";
import eventsRoutes from "./routes/events";
import invalidateRoutes from "./routes/invalidate";
import mediaRoutes from "./routes/media";
import postsRoutes from "./routes/posts";
import tagsRoutes from "./routes/tags";
import type { ApiKeyApp, Env } from "./types/env";
const app = new OpenAPIHono<{ Bindings: Env }>();
// Global middleware — CORS must be first so preflight and cross-origin responses work
app.use(
"*",
cors({
origin: (origin) => {
if (!origin) {
return "*";
}
if (FRAMER_PLUGIN_PATTERN.test(origin)) {
return origin;
}
return "*";
},
allowHeaders: ["Content-Type", "Authorization"],
allowMethods: ["GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"],
})
);
app.use("*", cache());
app.use(trimTrailingSlash());
// Internal System Routes (no API key, no analytics)
app.use("/cache/invalidate", systemAuth());
app.route("/cache/invalidate", cacheRoutes);
app.use("/internal/events", systemAuth());
app.route("/internal/events", eventsRoutes);
// Legacy Workspace ID Routes (/v1/:workspaceId/*)
// MUST be registered BEFORE apiKeyV1 to intercept workspace ID routes
// Using standard Hono since these are deprecated and don't need to be in the spec
const legacyV1 = new Hono<{ Bindings: Env }>();
legacyV1.use("/:workspaceId/*", ratelimit("workspace"));
legacyV1.use("/:workspaceId/*", authorization());
legacyV1.use("/:workspaceId/*", legacyAnalytics());
legacyV1.route("/:workspaceId/tags", tagsRoutes);
legacyV1.route("/:workspaceId/categories", categoriesRoutes);
legacyV1.route("/:workspaceId/posts", postsRoutes);
legacyV1.route("/:workspaceId/authors", authorsRoutes);
// Mount legacy routes dispatcher - intercepts /v1/:workspaceId/* BEFORE apiKeyV1
app.use("/v1/:workspaceId/*", async (c, next) => {
const path = c.req.path;
const workspaceId = c.req.param("workspaceId");
// Check if this is a legacy workspace route (workspaceId is not a known resource)
if (!ROUTES.includes(workspaceId)) {
// Rewrite path (strip /v1 prefix) for legacy router
const newPath = path.replace("/v1", "");
const newUrl = new URL(c.req.url);
newUrl.pathname = newPath;
const newRequest = new Request(newUrl.toString(), c.req.raw);
return legacyV1.fetch(newRequest, c.env, c.executionCtx);
}
// If workspaceId is actually a resource name (posts, tags, etc.), skip this middleware
// and let Hono continue to the next matching route (apiKeyV1)
return next();
});
// API Key Routes (/v1/posts, /v1/tags, etc.)
// Using OpenAPIHono to properly merge specs
const apiKeyV1 = new OpenAPIHono<ApiKeyApp>();
apiKeyV1.use("*", ratelimit("apiKey"));
apiKeyV1.use("*", keyAuthorization());
apiKeyV1.use("*", analytics());
// Mount routes with proper OpenAPIHono to enable spec merging
apiKeyV1.route("/posts", postsRoutes);
apiKeyV1.route("/categories", categoriesRoutes);
apiKeyV1.route("/tags", tagsRoutes);
apiKeyV1.route("/authors", authorsRoutes);
apiKeyV1.route("/media", mediaRoutes);
apiKeyV1.route("/cache/invalidate", invalidateRoutes);
// Mount apiKeyV1 under /v1 to automatically merge OpenAPI specs
app.route("/v1", apiKeyV1);
// Redirect non-versioned routes to v1
app.use("/:workspaceId/*", async (c, next) => {
const path = c.req.path;
const workspaceId = c.req.param("workspaceId");
if (
path.startsWith("/v1/") ||
path === "/" ||
path === "/status" ||
path === "/openapi.json"
) {
return next();
}
const isWorkspaceRoute = ROUTES.some(
(route) =>
path === `/${workspaceId}/${route}` ||
path.startsWith(`/${workspaceId}/${route}/`)
);
if (isWorkspaceRoute) {
const url = new URL(c.req.url);
url.pathname = `/v1${path}`;
return Response.redirect(url.toString(), 308);
}
return next();
});
// Redirect non-versioned API routes to v1 (e.g., /posts -> /v1/posts)
app.use("/*", async (c, next) => {
const path = c.req.path;
const firstSegment = path.split("/").filter(Boolean)[0];
if (firstSegment && ROUTES.includes(firstSegment)) {
const url = new URL(c.req.url);
url.pathname = `/v1${path}`;
return Response.redirect(url.toString(), 308);
}
return next();
});
app.get("/", (c) => c.text("Hello from marble"));
app.get("/status", (c) => c.json({ status: "ok" }));
app.doc("/openapi.json", {
openapi: "3.1.0",
info: {
title: "Marble API",
version: "1.0.0",
description:
"A headless CMS API for managing and delivering content programmatically.",
},
servers: [{ url: "https://api.marblecms.com", description: "Production" }],
security: [{ apiKey: [] }],
});
app.openAPIRegistry.registerComponent("securitySchemes", "apiKey", {
type: "apiKey",
in: "header",
name: "Authorization",
description: "Your Marble API key",
});
export default app;
================================================
FILE: apps/api/src/index.ts
================================================
/** biome-ignore-all lint/performance/noBarrelFile: "required" */
export { default } from "./app";
================================================
FILE: apps/api/src/lib/cache.ts
================================================
import { Redis } from "@upstash/redis/cloudflare";
/** Default cache TTL in seconds (1 hour) */
const DEFAULT_TTL = 3600;
/** Cache key prefix for all cached data */
const CACHE_PREFIX = "cache";
/** Skip caching values larger than 8MB to stay under Upstash's 10MB request limit */
const MAX_CACHE_VALUE_BYTES = 8 * 1024 * 1024;
export type CacheClient = ReturnType<typeof createCacheClient>;
/**
* Create a cache client with helper methods for the cache-aside pattern.
* Uses Upstash Redis for storage.
*/
export function createCacheClient(url: string, token: string) {
const redis = new Redis({ url, token });
return {
/**
* Get a cached value by key
*/
async get<T>(key: string): Promise<T | null> {
try {
const value = await redis.get<T>(key);
if (value !== null) {
console.log(`[Cache] HIT: ${key}`);
}
return value;
} catch (error) {
console.error(`[Cache] GET error for ${key}:`, error);
return null;
}
},
/**
* Set a cached value with optional TTL
*/
async set<T>(key: string, value: T, ttl = DEFAULT_TTL): Promise<void> {
try {
const serialized = JSON.stringify(value);
if (serialized.length > MAX_CACHE_VALUE_BYTES) {
console.warn(
`[Cache] SKIP SET: ${key} is ${serialized.length} bytes, exceeds ${MAX_CACHE_VALUE_BYTES} byte limit`
);
return;
}
await redis.set(key, value, { ex: ttl });
console.log(`[Cache] SET: ${key} (TTL: ${ttl}s)`);
} catch (error) {
console.error(`[Cache] SET error for ${key}:`, error);
}
},
/**
* Cache-aside pattern: get from cache or fetch and cache
*/
async getOrSet<T>(
key: string,
fetcher: () => Promise<T>,
ttl = DEFAULT_TTL
): Promise<T> {
try {
const cached = await redis.get<T>(key);
if (cached !== null) {
console.log(`[Cache] HIT: ${key}`);
return cached;
}
console.log(`[Cache] MISS: ${key}`);
const fresh = await fetcher();
const serialized = JSON.stringify(fresh);
if (serialized.length > MAX_CACHE_VALUE_BYTES) {
console.warn(
`[Cache] SKIP SET: ${key} is ${serialized.length} bytes, exceeds ${MAX_CACHE_VALUE_BYTES} byte limit`
);
return fresh;
}
await redis.set(key, fresh, { ex: ttl });
return fresh;
} catch (error) {
console.error(`[Cache] getOrSet error for ${key}:`, error);
return fetcher();
}
},
/**
* Cache-aside pattern for count queries
* Optimized for caching numeric count values
*/
async getOrSetCount<T extends number>(
key: string,
fetcher: () => Promise<T>,
ttl = DEFAULT_TTL
): Promise<T> {
try {
const cached = await redis.get<T>(key);
if (cached !== null) {
console.log(`[Cache] HIT (count): ${key}`);
return cached;
}
console.log(`[Cache] MISS (count): ${key}`);
const fresh = await fetcher();
await redis.set(key, fresh, { ex: ttl });
return fresh;
} catch (error) {
console.error(`[Cache] getOrSetCount error for ${key}:`, error);
return fetcher();
}
},
/**
* Invalidate cache keys matching a pattern
* Uses SCAN to find keys iteratively, then DEL to remove them in batches
*/
async invalidate(pattern: string): Promise<number> {
try {
let cursor: string | number = "0";
const allKeys: string[] = [];
const batchSize = 100;
// Use SCAN to iterate through keys matching the pattern
do {
const [nextCursor, keys]: [string | number, string[]] =
await redis.scan(cursor, {
match: pattern,
count: batchSize,
});
cursor = nextCursor;
if (Array.isArray(keys)) {
allKeys.push(...keys);
}
} while (String(cursor) !== "0");
// Delete keys in batches to avoid large argument lists
let deletedCount = 0;
for (let i = 0; i < allKeys.length; i += batchSize) {
const batch = allKeys.slice(i, i + batchSize);
if (batch.length > 0) {
const deleted = await redis.del(...batch);
deletedCount += deleted;
}
}
if (deletedCount > 0) {
console.log(`[Cache] INVALIDATE: ${pattern} (${deletedCount} keys)`);
}
return deletedCount;
} catch (error) {
console.error(`[Cache] INVALIDATE error for ${pattern}:`, error);
return 0;
}
},
/**
* Invalidate all cache for a specific workspace
*/
async invalidateWorkspace(workspaceId: string): Promise<number> {
return this.invalidate(`${CACHE_PREFIX}:${workspaceId}:*`);
},
/**
* Invalidate cache for a specific resource type in a workspace
*/
async invalidateResource(
workspaceId: string,
resource: "posts" | "categories" | "tags" | "authors" | "media"
): Promise<number> {
return this.invalidate(`${CACHE_PREFIX}:${workspaceId}:${resource}:*`);
},
};
}
/**
* Generate a cache key for a workspace resource
*/
export function cacheKey(
workspaceId: string,
resource: string,
...parts: string[]
): string {
return [CACHE_PREFIX, workspaceId, resource, ...parts].join(":");
}
/**
* Generate a hash from query parameters for cache key uniqueness
* Uses a polynomial rolling hash that works in both Node and Workers
*/
export function hashQueryParams(params: Record<string, unknown>): string {
const sorted = Object.keys(params)
.sort()
.reduce(
(acc, key) => {
const value = params[key];
if (value !== undefined && value !== null && value !== "") {
acc[key] = value;
}
return acc;
},
{} as Record<string, unknown>
);
// Use a polynomial rolling hash for consistent cross-platform hashing
const str = JSON.stringify(sorted);
const hash = Array.from(str).reduce((acc, char) => {
const code = char.charCodeAt(0);
return Math.abs((acc * 31 + code) % 2_147_483_647);
}, 0);
return hash.toString(36);
}
================================================
FILE: apps/api/src/lib/constants.ts
================================================
/**
* Available API route resources.
* Used for route dispatching and redirect logic.
*/
export const ROUTES = [
"posts",
"categories",
"tags",
"authors",
"cache",
"media",
];
export const MAX_UPLOAD_SIZE = 5 * 1024 * 1024;
export const DEFAULT_CDN_URL = "https://cdn.marblecms.com";
export const ALLOWED_IMAGE_MIME_TYPES = [
"image/jpeg",
"image/png",
"image/gif",
"image/webp",
"image/avif",
"image/svg+xml",
] as const;
export const ALLOWED_VIDEO_MIME_TYPES = [
"video/mp4",
"video/webm",
"video/ogg",
"video/quicktime",
] as const;
export const ALLOWED_AUDIO_MIME_TYPES = [
"audio/mpeg",
"audio/mp4",
"audio/ogg",
"audio/wav",
"audio/webm",
"audio/aac",
"audio/flac",
] as const;
export const ALLOWED_DOCUMENT_MIME_TYPES = [
"application/pdf",
"text/plain",
"text/csv",
"application/json",
"application/zip",
"application/msword",
"application/vnd.openxmlformats-officedocument.wordprocessingml.document",
"application/vnd.ms-excel",
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
"application/vnd.ms-powerpoint",
"application/vnd.openxmlformats-officedocument.presentationml.presentation",
] as const;
export const ALLOWED_MEDIA_MIME_TYPES = [
...ALLOWED_IMAGE_MIME_TYPES,
...ALLOWED_VIDEO_MIME_TYPES,
...ALLOWED_AUDIO_MIME_TYPES,
...ALLOWED_DOCUMENT_MIME_TYPES,
] as const;
export type AllowedMediaMimeType = (typeof ALLOWED_MEDIA_MIME_TYPES)[number];
export const FRAMER_PLUGIN_ID = "4pj5owtk2qcexo6c1yt9kicye";
export const FRAMER_PLUGIN_PATTERN = new RegExp(
`^https://${FRAMER_PLUGIN_ID}(-[a-zA-Z0-9]+)?\\.plugins\\.framercdn\\.com$`
);
================================================
FILE: apps/api/src/lib/crypto.ts
================================================
/**
* Web Crypto API utilities for Cloudflare Workers
* These functions use the native Web Crypto API instead of Node.js crypto
* to avoid WASM polyfill issues in the Workers runtime
*/
/**
* Hash an API key using SHA-256 via Web Crypto API
* @param key - The plaintext API key to hash
* @returns The SHA-256 hash of the key as a hex string (64 characters)
*/
export async function hashApiKey(key: string): Promise<string> {
const encoder = new TextEncoder();
const data = encoder.encode(key);
const hashBuffer = await crypto.subtle.digest("SHA-256", data);
const hashArray = Array.from(new Uint8Array(hashBuffer));
return hashArray.map((b) => b.toString(16).padStart(2, "0")).join("");
}
================================================
FILE: apps/api/src/lib/db.ts
================================================
import { createClient as createHyperdriveClient } from "@marble/db/hyperdrive";
import { createClient as createWorkersClient } from "@marble/db/workers";
import type { Env } from "@/types/env";
/**
* Get the database connection string.
* In development, uses DATABASE_URL directly to bypass Hyperdrive's local proxy
* (which can have compatibility issues with Neon's serverless driver).
* In production, uses Hyperdrive for connection pooling and latency optimization.
*/
export function getConnectionString(env: Env): string {
// if (env.ENVIRONMENT === "development" && env.DATABASE_URL) {
// return env.DATABASE_URL;
// }
if (!env.HYPERDRIVE?.connectionString) {
throw new Error(
"Database configuration error: no connection string available"
);
}
return env.HYPERDRIVE.connectionString;
}
/**
* Create a Prisma client with the correct adapter for the current env.
* - DATABASE_URL (dev or BYPASS_HYPERDRIVE): Neon serverless driver
* - HYPERDRIVE: pg-worker driver (standard Postgres, Hyperdrive-compatible)
*/
export type DbClient = ReturnType<typeof createDbClient>;
export function createDbClient(env: Env) {
// const useDirect = env.ENVIRONMENT === "development";
// if (useDirect && env.DATABASE_URL) {
// return createWorkersClient(env.DATABASE_URL);
// }
if (!env.HYPERDRIVE?.connectionString) {
throw new Error(
"Database configuration error: no connection string available"
);
}
return createHyperdriveClient(env.HYPERDRIVE.connectionString);
}
================================================
FILE: apps/api/src/lib/events.ts
================================================
import type { createDbClient } from "@/lib/db";
import type { JsonObject } from "@/validations/json";
import type {
WORKSPACE_EVENT_ACTOR_TYPES,
WORKSPACE_EVENT_RESOURCE_TYPES,
WORKSPACE_EVENT_SOURCES,
WORKSPACE_EVENT_TYPES,
} from "@/validations/misc";
interface EmitEventOptions {
type: (typeof WORKSPACE_EVENT_TYPES)[number];
workspaceId: string;
resourceType: (typeof WORKSPACE_EVENT_RESOURCE_TYPES)[number];
resourceId: string;
source?: (typeof WORKSPACE_EVENT_SOURCES)[number];
actorType?: (typeof WORKSPACE_EVENT_ACTOR_TYPES)[number];
actorId?: string;
payload?: JsonObject;
}
export async function emitEvent(
db: ReturnType<typeof createDbClient>,
queue: Queue,
options: EmitEventOptions
) {
const event = await db.workspaceEvent.create({
data: {
type: options.type,
workspaceId: options.workspaceId,
source: options.source ?? "api",
resourceType: options.resourceType,
resourceId: options.resourceId,
actorType: options.actorType,
actorId: options.actorId,
payload: options.payload ?? {},
},
});
await queue.send({ eventId: event.id });
return event;
}
================================================
FILE: apps/api/src/lib/media.ts
================================================
import { imageSize } from "image-size";
import { DEFAULT_CDN_URL } from "./constants";
export type MediaType = "image" | "video" | "audio" | "document";
export interface MediaRecord {
id: string;
name: string;
url: string;
alt: string | null;
size: number;
mimeType: string | null;
width: number | null;
height: number | null;
duration: number | null;
blurHash: string | null;
type: MediaType;
createdAt: Date;
updatedAt: Date;
}
export function serializeMedia(item: MediaRecord) {
return {
id: item.id,
name: item.name,
url: item.url,
alt: item.alt,
size: item.size,
mimeType: item.mimeType,
width: item.width,
height: item.height,
duration: item.duration,
blurHash: item.blurHash,
type: item.type,
createdAt: item.createdAt.toISOString(),
updatedAt: item.updatedAt.toISOString(),
};
}
export function getMediaType(mimeType: string): MediaType {
if (mimeType.startsWith("image/")) {
return "image";
}
if (mimeType.startsWith("video/")) {
return "video";
}
if (mimeType.startsWith("audio/")) {
return "audio";
}
return "document";
}
export function extensionFromFile(file: File) {
const filename = file.name.trim();
const filenameExtension = filename.includes(".")
? filename.split(".").pop()
: undefined;
if (filenameExtension) {
return filenameExtension.toLowerCase().replace(/[^a-z0-9]/g, "");
}
return file.type.split("/")[1]?.split("+")[0] || "bin";
}
export function publicUrl(envUrl: string | undefined, key: string) {
const base = (envUrl || DEFAULT_CDN_URL).replace(/\/$/, "");
return `${base}/${key}`;
}
export function objectKeyFromUrl(url: string) {
try {
return new URL(url).pathname.replace(/^\/+/, "");
} catch {
return null;
}
}
export function getImageDimensions(buffer: ArrayBuffer) {
try {
const dimensions = imageSize(new Uint8Array(buffer));
return {
width: dimensions.width,
height: dimensions.height,
};
} catch (error) {
console.warn("Failed to read image dimensions:", error);
return {};
}
}
================================================
FILE: apps/api/src/lib/polar.ts
================================================
import { Polar } from "@polar-sh/sdk";
export function createPolarClient(accessToken: string, isProduction = false) {
return new Polar({
server: isProduction ? "production" : "sandbox",
accessToken,
});
}
================================================
FILE: apps/api/src/lib/posts.ts
================================================
import { z } from "@hono/zod-openapi";
export const buildStatusFilter = (status: "published" | "draft" | "all") =>
status === "all"
? { status: { in: ["published", "draft"] as ("published" | "draft")[] } }
: { status };
function castFieldValue(
value: string,
type: string
): string | number | boolean | string[] | null {
switch (type) {
case "number": {
const num = Number.parseFloat(value);
return Number.isNaN(num) ? null : num;
}
case "boolean":
return value === "true";
case "multiselect":
try {
return z.array(z.string()).parse(JSON.parse(value));
} catch {
return null;
}
default:
return value;
}
}
export function buildFieldsObject(
fieldValues: Array<{
value: string;
field: { key: string; type: string };
}>,
allFields?: Array<{ key: string; type: string }>
): Record<string, string | number | boolean | string[] | null> {
const result: Record<string, string | number | boolean | string[] | null> =
{};
if (allFields) {
for (const field of allFields) {
result[field.key] = null;
}
}
for (const fieldValue of fieldValues) {
result[fieldValue.field.key] = castFieldValue(
fieldValue.value,
fieldValue.field.type
);
}
return result;
}
================================================
FILE: apps/api/src/lib/redis.ts
================================================
import { Redis } from "@upstash/redis/cloudflare";
export function createRedisClient(url: string, token: string): Redis {
return new Redis({ url, token });
}
================================================
FILE: apps/api/src/lib/sanitize.ts
================================================
import sanitize, { defaults } from "sanitize-html";
/**
* Sanitize HTML content to prevent XSS attacks.
* Uses the same configuration as the CMS editor to ensure consistency.
*
* - Strips `<script>` tags and `on*` event handlers
* - Whitelists safe HTML tags and attributes
* - Only allows safe URL schemes (blocks `javascript:` in hrefs)
* - Restricts iframe sources to YouTube only
*/
export const sanitizeHtml = (content: string): string => {
return sanitize(content, {
allowedTags: [
"b",
"i",
"em",
"strong",
"a",
"img",
"video",
"track",
"h1",
"h2",
"h3",
"h4",
"h5",
"h6",
"code",
"pre",
"p",
"li",
"ul",
"ol",
"blockquote",
"td",
"th",
"table",
"tr",
"tbody",
"thead",
"tfoot",
"small",
"div",
"iframe",
"input",
"label",
"figure",
"figcaption",
"span",
"mark",
"s",
"u",
"sub",
"sup",
"hr",
],
allowedAttributes: {
...defaults.allowedAttributes,
"*": ["style"],
code: ["class"],
a: ["href", "target"],
iframe: ["src", "allowfullscreen", "style", "width", "height"],
input: ["type", "checked"],
figure: [
"src",
"alt",
"data-width",
"caption",
"data-align",
"data-type",
],
video: ["src", "controls", "preload", "muted", "loop", "playsinline"],
track: ["kind", "src", "srclang", "label"],
div: ["data-twitter", "data-src", "data-youtube-video"],
span: ["style", "data-color"],
mark: ["style", "data-color"],
},
allowedStyles: {
"*": {
color: [
/^#[\da-fA-F]{3,6}$/,
/^rgb\(\s*\d+\s*,\s*\d+\s*,\s*\d+\s*\)$/,
/^rgba\(\s*\d+\s*,\s*\d+\s*,\s*\d+\s*,\s*[\d.]+\s*\)$/,
/^[a-zA-Z]+$/,
],
"background-color": [
/^#[\da-fA-F]{3,6}$/,
/^rgb\(\s*\d+\s*,\s*\d+\s*,\s*\d+\s*\)$/,
/^rgba\(\s*\d+\s*,\s*\d+\s*,\s*\d+\s*,\s*[\d.]+\s*\)$/,
/^[a-zA-Z]+$/,
],
"text-decoration": [/^line-through$/, /^underline$/, /^none$/],
},
},
allowedSchemes: ["http", "https", "ftp", "mailto"],
allowedSchemesByTag: {
img: ["http", "https", "data"],
video: ["http", "https"],
a: ["http", "https", "ftp", "mailto"],
iframe: ["https"],
},
allowedIframeHostnames: ["www.youtube.com", "www.youtube-nocookie.com"],
exclusiveFilter: (frame) => {
if (frame.tag === "script") {
return true;
}
if (frame.tag === "input" && frame.attribs?.type !== "checkbox") {
return true;
}
if (frame.attribs) {
for (const attr in frame.attribs) {
if (/^on/i.test(attr)) {
return true;
}
}
}
return false;
},
});
};
================================================
FILE: apps/api/src/lib/usage.ts
================================================
import { sendUsageLimitEmail } from "@marble/email";
import { getWorkspacePlan, PLAN_LIMITS, type PlanType } from "@marble/utils";
import { Redis } from "@upstash/redis/cloudflare";
import { Resend } from "resend";
import type { createDbClient } from "@/lib/db";
type DbClient = ReturnType<typeof createDbClient>;
const USAGE_KEY_PREFIX = "usage:api";
const USAGE_META_PREFIX = "usage:meta";
const META_TTL = 300;
interface UsageMeta {
limit: number;
plan: PlanType;
periodEnd: string;
}
interface BillingPeriod {
start: Date;
end: Date;
}
async function getBillingPeriod(
db: DbClient,
workspaceId: string
): Promise<BillingPeriod> {
const workspace = await db.organization.findUnique({
where: { id: workspaceId },
select: {
createdAt: true,
subscriptions: {
where: { status: { in: ["active", "trialing", "canceled"] } },
orderBy: { createdAt: "desc" },
take: 1,
select: {
status: true,
cancelAtPeriodEnd: true,
currentPeriodStart: true,
currentPeriodEnd: true,
},
},
},
});
if (!workspace) {
const now = new Date();
return {
start: new Date(now.getFullYear(), now.getMonth(), 1),
end: new Date(now.getFullYear(), now.getMonth() + 1, 1),
};
}
const subscription = workspace.subscriptions[0];
const isValid =
subscription &&
(subscription.status === "active" ||
subscription.status === "trialing" ||
(subscription.status === "canceled" &&
subscription.cancelAtPeriodEnd &&
subscription.currentPeriodEnd &&
subscription.currentPeriodEnd > new Date()));
if (
isValid &&
subscription.currentPeriodStart &&
subscription.currentPeriodEnd
) {
return {
start: subscription.currentPeriodStart,
end: subscription.currentPeriodEnd,
};
}
const dayOfMonth = workspace.createdAt.getDate();
const now = new Date();
const getValidDate = (year: number, month: number, day: number) => {
const lastDay = new Date(year, month + 1, 0).getDate();
return new Date(year, month, Math.min(day, lastDay));
};
let periodStart = getValidDate(now.getFullYear(), now.getMonth(), dayOfMonth);
if (periodStart > now) {
periodStart = getValidDate(
now.getFullYear(),
now.getMonth() - 1,
dayOfMonth
);
}
const periodEnd = getValidDate(
periodStart.getFullYear(),
periodStart.getMonth() + 1,
dayOfMonth
);
return { start: periodStart, end: periodEnd };
}
export interface UsageCheckResult {
allowed: boolean;
currentUsage: number;
limit: number;
percentage: number;
plan: PlanType;
thresholdCrossed?: 75 | 90 | 100;
}
async function getUsageMeta(
redis: Redis,
db: DbClient,
workspaceId: string
): Promise<UsageMeta> {
const metaKey = `${USAGE_META_PREFIX}:${workspaceId}`;
const cached = await redis.get<UsageMeta>(metaKey);
if (cached) {
return cached;
}
const workspace = await db.organization.findUnique({
where: { id: workspaceId },
select: {
subscriptions: {
where: { status: { in: ["active", "trialing", "canceled"] } },
orderBy: { createdAt: "desc" },
take: 1,
select: {
plan: true,
status: true,
cancelAtPeriodEnd: true,
currentPeriodEnd: true,
},
},
},
});
const subscription = workspace?.subscriptions[0];
const plan = getWorkspacePlan(subscription);
const limit = PLAN_LIMITS[plan].maxApiRequests;
const period = await getBillingPeriod(db, workspaceId);
const meta: UsageMeta = {
limit,
plan,
periodEnd: period.end.toISOString(),
};
await redis.set(metaKey, meta, { ex: META_TTL });
return meta;
}
async function seedUsageCounter(
redis: Redis,
db: DbClient,
workspaceId: string,
periodEnd: Date
): Promise<number> {
const period = await getBillingPeriod(db, workspaceId);
const count = await db.usageEvent.count({
where: {
workspaceId,
type: "api_request",
createdAt: { gte: period.start, lt: period.end },
},
});
const counterKey = `${USAGE_KEY_PREFIX}:${workspaceId}`;
const ttl = Math.max(
1,
Math.floor((periodEnd.getTime() - Date.now()) / 1000)
);
await redis.set(counterKey, count, { ex: ttl });
return count;
}
export async function checkApiUsage(
db: DbClient,
workspaceId: string,
redisCredentials?: { url: string; token: string }
): Promise<UsageCheckResult> {
if (!redisCredentials) {
return checkApiUsageFromDb(db, workspaceId);
}
const redis = new Redis({
url: redisCredentials.url,
token: redisCredentials.token,
});
try {
const meta = await getUsageMeta(redis, db, workspaceId);
const counterKey = `${USAGE_KEY_PREFIX}:${workspaceId}`;
const exists = await redis.exists(counterKey);
let currentUsage: number;
if (exists) {
currentUsage = await redis.incr(counterKey);
currentUsage -= 1;
} else {
currentUsage = await seedUsageCounter(
redis,
db,
workspaceId,
new Date(meta.periodEnd)
);
}
const percentage = meta.limit > 0 ? (currentUsage / meta.limit) * 100 : 0;
let thresholdCrossed: 75 | 90 | 100 | undefined;
const nextPercentage =
meta.limit > 0 ? ((currentUsage + 1) / meta.limit) * 100 : 0;
if (nextPercentage >= 100 && percentage < 100) {
thresholdCrossed = 100;
} else if (nextPercentage >= 90 && percentage < 90) {
thresholdCrossed = 90;
} else if (nextPercentage >= 75 && percentage < 75) {
thresholdCrossed = 75;
}
return {
allowed: currentUsage < meta.limit,
currentUsage,
limit: meta.limit,
percentage,
plan: meta.plan,
thresholdCrossed,
};
} catch (err) {
console.error("[ApiUsage] Redis error, falling back to DB:", err);
return checkApiUsageFromDb(db, workspaceId);
}
}
async function checkApiUsageFromDb(
db: DbClient,
workspaceId: string
): Promise<UsageCheckResult> {
const workspace = await db.organization.findUnique({
where: { id: workspaceId },
select: {
subscriptions: {
where: { status: { in: ["active", "trialing", "canceled"] } },
orderBy: { createdAt: "desc" },
take: 1,
select: {
plan: true,
status: true,
cancelAtPeriodEnd: true,
currentPeriodEnd: true,
},
},
},
});
const subscription = workspace?.subscriptions[0];
const plan = getWorkspacePlan(subscription);
const limit = PLAN_LIMITS[plan].maxApiRequests;
const period = await getBillingPeriod(db, workspaceId);
const currentUsage = await db.usageEvent.count({
where: {
workspaceId,
type: "api_request",
createdAt: { gte: period.start, lt: period.end },
},
});
const percentage = limit > 0 ? (currentUsage / limit) * 100 : 0;
let thresholdCrossed: 75 | 90 | 100 | undefined;
const nextPercentage = limit > 0 ? ((currentUsage + 1) / limit) * 100 : 0;
if (nextPercentage >= 100 && percentage < 100) {
thresholdCrossed = 100;
} else if (nextPercentage >= 90 && percentage < 90) {
thresholdCrossed = 90;
} else if (nextPercentage >= 75 && percentage < 75) {
thresholdCrossed = 75;
}
return {
allowed: currentUsage < limit,
currentUsage,
limit,
percentage,
plan,
thresholdCrossed,
};
}
export async function notifyApiUsageThreshold(
resendApiKey: string,
db: DbClient,
workspaceId: string,
threshold: 75 | 90 | 100,
currentUsage: number,
limit: number
): Promise<void> {
const owner = await db.member.findFirst({
where: { organizationId: workspaceId, role: "owner" },
select: {
user: { select: { email: true, name: true } },
},
});
if (!owner?.user) {
console.warn(
`[ApiUsage] No owner found for workspace ${workspaceId}, skipping notification`
);
return;
}
try {
const resend = new Resend(resendApiKey);
await sendUsageLimitEmail(resend, {
userEmail: owner.user.email,
userName: owner.user.name,
featureName: "API Requests",
usageAmount: currentUsage,
limitAmount: limit,
workspaceId,
});
console.log(
`[ApiUsage] Sent ${threshold}% threshold email for workspace ${workspaceId}`
);
} catch (error) {
console.error("[ApiUsage] Failed to send threshold notification:", error);
}
}
================================================
FILE: apps/api/src/lib/workspace.ts
================================================
import type { Context } from "hono";
import { HTTPException } from "hono/http-exception";
/**
* Get the workspace ID from either context (API key routes) or URL params (legacy routes)
* This allows route handlers to work with both authentication methods
* @returns workspaceId or undefined if not found
*/
export const getWorkspaceId = (c: Context): string | undefined => {
// Try context first (set by keyAuthorization middleware for API key routes)
const contextWorkspaceId = c.get("workspaceId") as string | undefined;
if (contextWorkspaceId) {
return contextWorkspaceId;
}
// Fall back to URL param (legacy workspace ID routes)
return c.req.param("workspaceId");
};
/**
* Get the workspace ID or throw if not found.
* Use this in route handlers to ensure workspaceId exists before database queries.
* @throws HTTPException 400 if workspaceId is missing
*/
export const requireWorkspaceId = (c: Context): string => {
const workspaceId = getWorkspaceId(c);
if (!workspaceId) {
throw new HTTPException(400, {
message: "Workspace ID is required",
});
}
return workspaceId;
};
================================================
FILE: apps/api/src/middleware/analytics.ts
================================================
import type { Context, MiddlewareHandler } from "hono";
import { createDbClient, type DbClient } from "@/lib/db";
import { createPolarClient } from "@/lib/polar";
import {
checkApiUsage,
notifyApiUsageThreshold,
type UsageCheckResult,
} from "@/lib/usage";
import type { ApiKeyApp } from "@/types/env";
interface AnalyticsTaskParams {
db: ReturnType<typeof createDbClient>;
workspaceId: string;
endpoint: string | null;
method: string;
status: number;
usageResult: UsageCheckResult | null;
resendApiKey?: string;
polarAccessToken?: string;
environment?: string;
apiKeyType?: string;
}
export async function runAnalyticsTask({
db,
workspaceId,
endpoint,
method,
status,
usageResult,
resendApiKey,
polarAccessToken,
environment,
apiKeyType,
}: AnalyticsTaskParams): Promise<void> {
try {
await db.usageEvent.create({
data: {
type: "api_request",
workspaceId,
endpoint,
},
});
if (resendApiKey && usageResult?.thresholdCrossed) {
try {
await notifyApiUsageThreshold(
resendApiKey,
db,
workspaceId,
usageResult.thresholdCrossed,
usageResult.currentUsage + 1,
usageResult.limit
);
} catch (usageError) {
console.error(
"[Analytics] Error sending usage threshold email:",
usageError
);
}
}
let customerId = workspaceId;
const organization = await db.organization.findFirst({
where: { id: workspaceId },
select: {
members: {
where: { role: "owner" },
select: { userId: true },
},
},
});
if (organization?.members[0]?.userId) {
customerId = organization.members[0].userId;
}
if (polarAccessToken) {
const isProduction = environment === "production";
const polar = createPolarClient(polarAccessToken, isProduction);
try {
await polar.events.ingest({
events: [
{
name: "api_request",
externalCustomerId: customerId,
metadata: {
...(endpoint && { endpoint }),
method,
status,
...(apiKeyType && { apiKeyType }),
},
},
],
});
} catch (polarError) {
if (polarError instanceof Error) {
console.error("[Analytics] Polar error:", polarError.message);
}
}
} else {
console.log(
"[Analytics] Skipping Polar: POLAR_ACCESS_TOKEN not configured"
);
}
} catch (err) {
console.error("[Analytics] Error in analytics task:", err);
}
}
async function checkUsage(
c: Context,
workspaceId: string
): Promise<UsageCheckResult | null> {
const { REDIS_URL, REDIS_TOKEN } = c.env;
if (!workspaceId) {
return null;
}
try {
const db = createDbClient(c.env);
const redis =
REDIS_URL && REDIS_TOKEN
? { url: REDIS_URL, token: REDIS_TOKEN }
: undefined;
const result = await checkApiUsage(db, workspaceId, redis);
if (!result.allowed) {
return result;
}
return result;
} catch (err) {
console.error("[Analytics] Error checking usage limits:", err);
return null;
}
}
/**
* Analytics middleware for API key authenticated routes.
* Checks usage limits before the request and logs analytics after.
*/
export const analytics = (): MiddlewareHandler<ApiKeyApp> => {
return async (c, next) => {
const method = c.req.method;
const workspaceId = c.get("workspaceId");
let usageResult: UsageCheckResult | null = null;
if (workspaceId && method !== "OPTIONS") {
usageResult = await checkUsage(c, workspaceId);
if (usageResult && !usageResult.allowed) {
return c.json(
{
error: "Usage limit exceeded",
message:
"You have reached your API request limit for this billing period. Please upgrade your plan or wait until your usage resets.",
},
429
);
}
}
await next();
const { RESEND_API_KEY, POLAR_ACCESS_TOKEN, ENVIRONMENT } = c.env;
let db: DbClient;
try {
db = createDbClient(c.env);
} catch {
console.error("[Analytics] Database configuration error");
return;
}
const apiKeyType = c.get("apiKeyType");
const status = c.res.status ?? 200;
if (!workspaceId || method === "OPTIONS" || status >= 400) {
return;
}
const path = c.req.path;
const pathParts = path.split("/").filter(Boolean);
const endpoint = pathParts.length >= 1 ? `/${pathParts.join("/")}` : null;
c.executionCtx?.waitUntil(
runAnalyticsTask({
db,
workspaceId,
endpoint,
method,
status,
usageResult,
resendApiKey: RESEND_API_KEY,
polarAccessToken: POLAR_ACCESS_TOKEN,
environment: ENVIRONMENT,
apiKeyType,
})
);
};
};
================================================
FILE: apps/api/src/middleware/authorization.ts
================================================
import type { Context, MiddlewareHandler, Next } from "hono";
import { createDbClient, type DbClient } from "@/lib/db";
export const authorization =
(): MiddlewareHandler => async (c: Context, next: Next) => {
let db: DbClient;
try {
db = createDbClient(c.env);
} catch {
console.error("[Authorization] Database configuration error");
return c.json({ error: "Internal server error" }, 500);
}
const workspaceId: string | null = c.req.param("workspaceId") ?? null;
if (!workspaceId) {
console.error("[Authorization] Workspace ID not found");
return c.json({ error: "Workspace ID is required" }, 400);
}
try {
const workspace = await db.organization.findUnique({
where: {
id: workspaceId,
},
select: {
id: true,
},
});
if (!workspace) {
return c.json(
{
error: "Invalid workspace",
message: "The provided workspace key is invalid or does not exist",
},
404
);
}
await next();
} catch (error) {
console.error("[Authorization] Error validating workspace:", error);
return c.json({ error: "Failed to validate workspace" }, 500);
}
};
================================================
FILE: apps/api/src/middleware/cache.ts
================================================
import type { MiddlewareHandler } from "hono";
/**
* Default stale-if-error time in seconds.
* This tells CDNs/browsers to serve stale content if the origin returns an error.
*/
const DEFAULT_STALE_IF_ERROR = 3600; // 1 hour
export interface CacheOptions {
/**
* Time in seconds for stale-if-error directive.
* When the origin returns an error, CDNs can serve cached content for this duration.
* @default 3600 (1 hour)
*/
staleIfError?: number;
}
/**
* Cache Control Middleware
*
* Automatically adds cache-related headers to successful GET/HEAD responses.
* Currently adds `stale-if-error` directive to allow CDNs to serve stale content
* when the origin returns errors.
*
* This middleware runs AFTER the route handler (post-processing) to inspect
* the response status and existing headers before adding cache directives.
*
* @example
* ```ts
* // Use with default options (1 hour stale-if-error)
* app.use("*", cache());
*
* // Use with custom stale-if-error time
* app.use("*", cache({ staleIfError: 7200 })); // 2 hours
* ```
*
* @param options - Configuration options for cache behavior
* @returns Hono middleware handler
*/
export const cache = (options: CacheOptions = {}): MiddlewareHandler => {
const staleIfError = options.staleIfError ?? DEFAULT_STALE_IF_ERROR;
return async (c, next) => {
await next();
const method = c.req.method;
// Only apply cache headers to GET and HEAD requests
if (method !== "GET" && method !== "HEAD") {
return;
}
// Only apply to successful responses (2xx and 3xx)
const status = c.res.status ?? 200;
if (status < 200 || status >= 400) {
return;
}
const existingCacheControl = c.res.headers.get("Cache-Control") ?? "";
// Skip if response explicitly opts out of caching
if (/\bno-store\b/i.test(existingCacheControl)) {
return;
}
// Skip if stale-if-error is already set
if (/\bstale-if-error\s*=\s*\d+\b/i.test(existingCacheControl)) {
return;
}
// Append stale-if-error to existing Cache-Control header
const newValue = existingCacheControl
? `${existingCacheControl}, stale-if-error=${staleIfError}`
: `stale-if-error=${staleIfError}`;
c.header("Cache-Control", newValue);
};
};
================================================
FILE: apps/api/src/middleware/key-authorization.ts
================================================
import type { MiddlewareHandler } from "hono";
import { hashApiKey } from "@/lib/crypto";
import { createDbClient, type DbClient } from "@/lib/db";
import type { ApiKeyApp } from "@/types/env";
/**
* API Key Authorization Middleware
* Verifies API keys from Authorization header or ?key= query parameter
* Sets workspaceId and apiKeyId in context for downstream use
*/
export const keyAuthorization =
(): MiddlewareHandler<ApiKeyApp> => async (c, next) => {
let db: DbClient;
try {
db = createDbClient(c.env);
} catch {
console.error("[KeyAuth] Database configuration error");
return c.json({ error: "Internal server error" }, 500);
}
let apiKey: string | null = null;
const authHeader = c.req.header("Authorization");
if (authHeader) {
if (authHeader.startsWith("Bearer ")) {
apiKey = authHeader.substring(7);
} else {
apiKey = authHeader;
}
}
if (!apiKey) {
apiKey = c.req.query("key") ?? null;
}
if (!apiKey) {
return c.json(
{
error: "Unauthorized",
message:
"API key required. Provide via Authorization header or ?key= query parameter",
},
401
);
}
try {
const hashedKey = await hashApiKey(apiKey);
const key = await db.apiKey.findUnique({
where: { key: hashedKey },
select: {
id: true,
workspaceId: true,
type: true,
scopes: true,
enabled: true,
expiresAt: true,
},
});
if (!key) {
return c.json(
{
error: "Unauthorized",
message: "Invalid API key",
},
401
);
}
if (!key.enabled) {
return c.json(
{
error: "Unauthorized",
message: "API key is disabled",
},
401
);
}
if (key.expiresAt && key.expiresAt < new Date()) {
return c.json(
{
error: "Unauthorized",
message: "API key has expired",
},
401
);
}
c.executionCtx?.waitUntil(
db.apiKey.update({
where: { id: key.id },
data: { lastUsed: new Date(), requestCount: { increment: 1 } },
})
);
c.set("workspaceId", key.workspaceId);
c.set("apiKeyId", key.id);
c.set("apiKeyType", key.type);
if (c.req.method !== "GET" && key.type !== "private") {
return c.json(
{
error: "Forbidden",
message:
"Write operations require a private API key (msk_...). Public keys are read-only.",
},
403
);
}
await next();
} catch (error) {
console.error("[KeyAuth] Error verifying API key:", error);
return c.json({ error: "Failed to verify API key" }, 500);
}
};
================================================
FILE: apps/api/src/middleware/legacy-analytics.ts
================================================
import type { Context, MiddlewareHandler, Next } from "hono";
import { createDbClient, type DbClient } from "@/lib/db";
import { checkApiUsage, type UsageCheckResult } from "@/lib/usage";
import { runAnalyticsTask } from "./analytics";
/**
* Legacy analytics middleware for workspace ID authenticated routes.
* Same as analytics() but reads workspaceId from URL params instead of context.
*/
export const legacyAnalytics = (): MiddlewareHandler => {
return async (c: Context, next: Next) => {
const method = c.req.method;
const workspaceId: string | null = c.req.param("workspaceId") ?? null;
const { REDIS_URL, REDIS_TOKEN } = c.env;
let usageResult: UsageCheckResult | null = null;
if (workspaceId && method !== "OPTIONS") {
try {
const db = createDbClient(c.env);
const redis =
REDIS_URL && REDIS_TOKEN
? { url: REDIS_URL, token: REDIS_TOKEN }
: undefined;
usageResult = await checkApiUsage(db, workspaceId, redis);
if (!usageResult.allowed) {
return c.json(
{
error: "Usage limit exceeded",
message:
"You have reached your API request limit for this billing period. Please upgrade your plan or wait until your usage resets.",
},
429
);
}
} catch (err) {
console.error("[LegacyAnalytics] Error checking usage limits:", err);
}
}
await next();
let db: DbClient;
try {
db = createDbClient(c.env);
} catch {
console.error("[LegacyAnalytics] Database configuration error");
return;
}
const status = c.res.status ?? 200;
if (!workspaceId || method === "OPTIONS" || status >= 400) {
return;
}
const path = c.req.path;
const pathParts = path.split("/").filter(Boolean);
const endpoint =
pathParts.length >= 3 ? `/${pathParts.slice(2).join("/")}` : null;
const { RESEND_API_KEY, POLAR_ACCESS_TOKEN, ENVIRONMENT } = c.env;
c.executionCtx?.waitUntil(
runAnalyticsTask({
db,
workspaceId,
endpoint,
method,
status,
usageResult,
resendApiKey: RESEND_API_KEY,
polarAccessToken: POLAR_ACCESS_TOKEN,
environment: ENVIRONMENT,
})
);
};
};
================================================
FILE: apps/api/src/middleware/ratelimit.ts
================================================
import { Ratelimit } from "@upstash/ratelimit";
import type { Context, MiddlewareHandler, Next } from "hono";
import { createRedisClient } from "@/lib/redis";
export interface RateLimit {
limit: number;
remaining: number;
reset: number;
success: boolean;
}
const cache = new Map();
type RateLimitMode = "workspace" | "apiKey";
/**
* Rate limiting middleware using Upstash Redis
* @param mode - "workspace" for legacy routes (IP + workspaceId), "apiKey" for API key routes
*/
export const ratelimit =
(mode: RateLimitMode = "workspace"): MiddlewareHandler =>
async (c: Context, next: Next) => {
try {
const redisClient = createRedisClient(c.env.REDIS_URL, c.env.REDIS_TOKEN);
const clientIp =
c.req.header("x-forwarded-for") ||
c.req.header("cf-connecting-ip") ||
"anonymous";
let identifier: string;
let limitConfig: ReturnType<typeof Ratelimit.slidingWindow>;
if (mode === "apiKey") {
// For API key routes, we rate limit by IP initially
// After keyAuthorization runs, we could enhance this
// For now: IP-based with higher limits since API keys are trusted
identifier = `apikey:${clientIp}`;
limitConfig = Ratelimit.slidingWindow(200, "10 s");
} else {
// Legacy workspace mode: IP + workspaceId
const workspaceId: string | null = c.req.param("workspaceId") ?? null;
identifier = workspaceId
? `${clientIp}:workspace:${workspaceId}`
: clientIp;
limitConfig = workspaceId
? Ratelimit.slidingWindow(200, "10 s")
: Ratelimit.slidingWindow(10, "10 s");
}
const rateLimiter = new Ratelimit({
redis: redisClient,
limiter: limitConfig,
ephemeralCache: cache,
});
const result = await rateLimiter.limit(identifier);
c.executionCtx.waitUntil(result.pending);
c.header("X-RateLimit-Limit", String(result.limit));
c.header("X-RateLimit-Remaining", String(result.remaining));
c.header("X-RateLimit-Reset", String(result.reset));
if (!result.success) {
return c.json({ error: "Too many requests" }, 429);
}
await next();
} catch (error) {
console.error("Rate limiting error:", error);
await next();
}
};
================================================
FILE: apps/api/src/middleware/system.ts
================================================
import type { MiddlewareHandler } from "hono";
import type { Env } from "@/types/env";
/**
* System Secret Authentication Middleware
* Validates X-System-Secret header for internal cache invalidation requests
*/
export const systemAuth =
(): MiddlewareHandler<{ Bindings: Env }> => async (c, next) => {
const systemSecret = c.env.SYSTEM_SECRET;
const providedSecret = c.req.header("X-System-Secret");
if (!systemSecret) {
console.error("[SystemAuth] SYSTEM_SECRET not configured");
return c.json({ error: "Internal server error" }, 500);
}
if (!providedSecret || providedSecret !== systemSecret) {
return c.json(
{
error: "Unauthorized",
message: "Invalid or missing system secret",
},
401
);
}
await next();
};
================================================
FILE: apps/api/src/routes/authors.ts
================================================
import { createRoute, OpenAPIHono, z } from "@hono/zod-openapi";
import { toAuthorPayload, withChanges } from "@marble/events";
import { cacheKey, createCacheClient, hashQueryParams } from "@/lib/cache";
import { createDbClient } from "@/lib/db";
import { emitEvent } from "@/lib/events";
import { requireWorkspaceId } from "@/lib/workspace";
import {
AuthorResponseSchema,
AuthorsListResponseSchema,
CreateAuthorBodySchema,
CreateAuthorResponseSchema,
UpdateAuthorBodySchema,
} from "@/schemas/authors";
import {
ConflictSchema,
DeleteResponseSchema,
ErrorSchema,
ForbiddenSchema,
LimitQuerySchema,
NotFoundSchema,
PageNotFoundSchema,
PageQuerySchema,
ServerErrorSchema,
} from "@/schemas/common";
import type { ApiKeyApp } from "@/types/env";
const authors = new OpenAPIHono<ApiKeyApp>();
const AuthorsQuerySchema = z.object({
limit: LimitQuerySchema,
page: PageQuerySchema,
});
const AuthorParamsSchema = z.object({
identifier: z.string().openapi({
param: { name: "identifier", in: "path" },
example: "john-doe",
description: "Author ID or slug",
}),
});
const listAuthorsRoute = createRoute({
method: "get",
path: "/",
tags: ["Authors"],
summary: "List authors",
description: "Get a paginated list of authors who have published posts",
request: {
query: AuthorsQuerySchema,
},
responses: {
200: {
content: { "application/json": { schema: AuthorsListResponseSchema } },
description: "Paginated list of authors",
},
400: {
content: {
"application/json": {
schema: z.union([ErrorSchema, PageNotFoundSchema]),
},
},
description: "Invalid query parameters or page number",
},
500: {
content: { "application/json": { schema: ServerErrorSchema } },
description: "Server error",
},
},
});
const getAuthorRoute = createRoute({
method: "get",
path: "/{identifier}",
tags: ["Authors"],
summary: "Get author",
description: "Get a single author by ID or slug",
request: {
params: AuthorParamsSchema,
},
responses: {
200: {
content: { "application/json": { schema: AuthorResponseSchema } },
description: "The requested author",
},
404: {
content: { "application/json": { schema: NotFoundSchema } },
description: "Author not found",
},
500: {
content: { "application/json": { schema: ServerErrorSchema } },
description: "Server error",
},
},
});
authors.openapi(listAuthorsRoute, async (c) => {
const workspaceId = requireWorkspaceId(c);
const db = createDbClient(c.env);
const cache = createCacheClient(c.env.REDIS_URL, c.env.REDIS_TOKEN);
const { limit, page } = c.req.valid("query");
// Generate cache key for count (exclude page - it doesn't affect count)
const countCacheKey = cacheKey(
workspaceId,
"authors",
"list",
hashQueryParams({ limit }),
"count"
);
// Cache count query separately (1 hour TTL, invalidated with posts)
const totalAuthors = await cache.getOrSetCount(countCacheKey, () =>
db.author.count({
where: {
workspaceId,
coAuthoredPosts: {
some: {
status: "published",
},
},
},
})
);
// Generate cache key for data (includes page)
const listCacheKey = cacheKey(
workspaceId,
"authors",
"list",
hashQueryParams({ page, limit })
);
const totalPages = Math.ceil(totalAuthors / limit);
const prevPage = page > 1 ? page - 1 : null;
const nextPage = page < totalPages ? page + 1 : null;
const authorsToSkip = limit ? (page - 1) * limit : 0;
if (page > totalPages && totalAuthors > 0) {
return c.json(
{
error: "Invalid page number" as const,
details: {
message: `Page ${page} does not exist.`,
totalPages,
requestedPage: page,
},
},
400 as const
);
}
try {
const authorsList = await cache.getOrSet(listCacheKey, () =>
db.author.findMany({
where: {
workspaceId,
coAuthoredPosts: {
some: {
status: "published",
},
},
},
select: {
id: true,
name: true,
image: true,
slug: true,
bio: true,
role: true,
socials: {
select: {
url: true,
platform: true,
},
},
_count: {
select: {
coAuthoredPosts: {
where: {
status: "published",
},
},
},
},
},
orderBy: [{ name: "asc" }],
take: limit,
skip: authorsToSkip,
})
);
// because I dont want prisma's ugly _count
const transformedAuthors = authorsList.map((author) => {
const { _count, ...rest } = author;
return {
...rest,
count: {
posts: _count.coAuthoredPosts,
},
};
});
return c.json(
{
authors: transformedAuthors,
pagination: {
limit,
currentPage: page,
nextPage,
previousPage: prevPage,
totalPages,
totalItems: totalAuthors,
},
},
200 as const
);
} catch (_error) {
return c.json({ error: "Failed to fetch authors" }, 500 as const);
}
});
authors.openapi(getAuthorRoute, async (c) => {
const workspaceId = requireWorkspaceId(c);
const { identifier } = c.req.valid("param");
const db = createDbClient(c.env);
const cache = createCacheClient(c.env.REDIS_URL, c.env.REDIS_TOKEN);
try {
// Cache by identifier (slug or id)
const singleCacheKey = cacheKey(workspaceId, "authors", identifier);
const author = await cache.getOrSet(singleCacheKey, () =>
db.author.findFirst({
where: {
workspaceId,
OR: [{ id: identifier }, { slug: identifier }],
},
select: {
id: true,
name: true,
image: true,
slug: true,
bio: true,
role: true,
socials: {
select: {
url: true,
platform: true,
},
},
},
})
);
if (!author) {
return c.json(
{
error: "Author not found",
message: "The requested author does not exist",
},
404 as const
);
}
return c.json({ author }, 200 as const);
} catch (_error) {
return c.json({ error: "Failed to fetch author" }, 500 as const);
}
});
// ─── POST /v1/authors ───
const createAuthorRoute = createRoute({
method: "post",
path: "/",
tags: ["Authors"],
summary: "Create author",
description:
"Create a new author. Requires a private API key. Hobby plan is limited to 1 author.",
request: {
body: {
content: { "application/json": { schema: CreateAuthorBodySchema } },
required: true,
},
},
responses: {
201: {
content: { "application/json": { schema: CreateAuthorResponseSchema } },
description: "Author created successfully",
},
400: {
content: { "application/json": { schema: ErrorSchema } },
description: "Invalid request body",
},
403: {
content: { "application/json": { schema: ForbiddenSchema } },
description:
"Public API key used for write operation or plan limit reached",
},
409: {
content: { "application/json": { schema: ConflictSchema } },
description: "Author with this slug already exists",
},
500: {
content: { "application/json": { schema: ServerErrorSchema } },
description: "Server error",
},
},
});
authors.openapi(createAuthorRoute, async (c) => {
try {
const db = createDbClient(c.env);
const workspaceId = requireWorkspaceId(c);
const cache = createCacheClient(c.env.REDIS_URL, c.env.REDIS_TOKEN);
const body = c.req.valid("json");
// Check plan limits — hobby plan limited to 1 author
const workspace = await db.organization.findUnique({
where: { id: workspaceId },
select: {
subscriptions: {
where: {
OR: [
{ status: "active" },
{ status: "trialing" },
{
status: "canceled",
cancelAtPeriodEnd: true,
currentPeriodEnd: { gt: new Date() },
},
],
},
orderBy: { createdAt: "desc" },
take: 1,
select: { plan: true },
},
},
});
const activeSub = workspace?.subscriptions[0];
const plan = activeSub?.plan?.toLowerCase() === "pro" ? "pro" : "hobby";
if (plan === "hobby") {
const existingCount = await db.author.count({
where: { workspaceId, isActive: true },
});
if (existingCount >= 1) {
return c.json(
{
error: "Author limit reached",
message:
"Hobby plan is limited to 1 author. Upgrade to Pro to create more.",
},
403 as const
);
}
}
// Check slug uniqueness
const existingAuthor = await db.author.findFirst({
where: { workspaceId, slug: body.slug },
});
if (existingAuthor) {
return c.json(
{
error: "Slug already in use",
message: "An author with this slug already exists in this workspace",
},
409 as const
);
}
const author = await db.author.create({
data: {
name: body.name,
slug: body.slug,
bio: body.bio ?? null,
role: body.role ?? null,
email: body.email ?? null,
image: body.image ?? null,
workspaceId,
...(body.socials &&
body.socials.length > 0 && {
socials: {
create: body.socials.map((s) => ({
url: s.url,
platform: s.platform,
})),
},
}),
},
select: {
id: true,
name: true,
slug: true,
bio: true,
role: true,
image: true,
socials: {
select: { url: true, platform: true },
},
},
});
c.executionCtx.waitUntil(cache.invalidateResource(workspaceId, "authors"));
const apiKeyId = c.get("apiKeyId");
c.executionCtx.waitUntil(
emitEvent(db, c.env.EVENT_QUEUE, {
type: "author_created",
workspaceId,
resourceType: "author",
resourceId: author.id,
actorType: "api_key",
actorId: apiKeyId,
payload: toAuthorPayload(author),
}).catch((error) => {
console.error("[authors.create] Failed to emit author_created:", error);
})
);
return c.json({ author }, 201 as const);
} catch (error) {
console.error("Error creating author:", error);
return c.json(
{
error: "Failed to create author",
message: "An unexpected error occurred",
},
500 as const
);
}
});
// ─── PATCH /v1/authors/{identifier} ───
const updateAuthorRoute = createRoute({
method: "patch",
path: "/{identifier}",
tags: ["Authors"],
summary: "Update author",
description:
"Update an existing author by ID or slug. Requires a private API key.",
request: {
params: AuthorParamsSchema,
body: {
content: { "application/json": { schema: UpdateAuthorBodySchema } },
required: true,
},
},
responses: {
200: {
content: { "application/json": { schema: CreateAuthorResponseSchema } },
description: "Author updated successfully",
},
400: {
content: { "application/json": { schema: ErrorSchema } },
description: "Invalid request body",
},
403: {
content: { "application/json": { schema: ForbiddenSchema } },
description: "Public API key used for write operation",
},
404: {
content: { "application/json": { schema: NotFoundSchema } },
description: "Author not found",
},
409: {
content: { "application/json": { schema: ConflictSchema } },
description: "Author with this slug already exists",
},
500: {
content: { "application/json": { schema: ServerErrorSchema } },
description: "Server error",
},
},
});
authors.openapi(updateAuthorRoute, async (c) => {
try {
const db = createDbClient(c.env);
const workspaceId = requireWorkspaceId(c);
const cache = createCacheClient(c.env.REDIS_URL, c.env.REDIS_TOKEN);
const { identifier } = c.req.valid("param");
const body = c.req.valid("json");
const existingAuthor = await db.author.findFirst({
where: {
workspaceId,
OR: [{ id: identifier }, { slug: identifier }],
},
});
if (!existingAuthor) {
return c.json(
{
error: "Author not found",
message: "The requested author does not exist",
},
404 as const
);
}
// If slug is being changed, check uniqueness
if (body.slug && body.slug !== existingAuthor.slug) {
const slugConflict = await db.author.findFirst({
where: {
slug: body.slug,
workspaceId,
id: { not: existingAuthor.id },
},
});
if (slugConflict) {
return c.json(
{
error: "Slug already in use",
message:
"An author with this slug already exists in this workspace",
},
409 as const
);
}
}
const updatedAuthor = await db.author.update({
where: { id: existingAuthor.id },
data: {
...(body.name !== undefined && { name: body.name }),
...(body.slug !== undefined && { slug: body.slug }),
...(body.bio !== undefined && { bio: body.bio }),
...(body.role !== undefined && { role: body.role }),
...(body.email !== undefined && { email: body.email || null }),
...(body.image !== undefined && { image: body.image }),
// Socials: delete all existing and recreate (same pattern as CMS)
...(body.socials !== undefined && {
socials: {
deleteMany: {},
...(body.socials.length > 0 && {
create: body.socials.map((s) => ({
url: s.url,
platform: s.platform,
})),
}),
},
}),
},
select: {
id: true,
name: true,
slug: true,
bio: true,
role: true,
image: true,
socials: {
select: { url: true, platform: true },
},
},
});
c.executionCtx.waitUntil(cache.invalidateResource(workspaceId, "authors"));
c.executionCtx.waitUntil(cache.invalidateResource(workspaceId, "posts"));
const apiKeyId = c.get("apiKeyId");
c.executionCtx.waitUntil(
emitEvent(db, c.env.EVENT_QUEUE, {
type: "author_updated",
workspaceId,
resourceType: "author",
resourceId: updatedAuthor.id,
actorType: "api_key",
actorId: apiKeyId,
payload: withChanges(toAuthorPayload(updatedAuthor), Object.keys(body)),
}).catch((error) => {
console.error("[authors.update] Failed to emit author_updated:", error);
})
);
return c.json({ author: updatedAuthor }, 200 as const);
} catch (error) {
console.error("Error updating author:", error);
return c.json(
{
error: "Failed to update author",
message: "An unexpected error occurred",
},
500 as const
);
}
});
// ─── DELETE /v1/authors/{identifier} ───
const deleteAuthorRoute = createRoute({
method: "delete",
path: "/{identifier}",
tags: ["Authors"],
summary: "Delete author",
description: "Delete an author by ID or slug. Requires a private API key.",
request: {
params: AuthorParamsSchema,
},
responses: {
200: {
content: { "application/json": { schema: DeleteResponseSchema } },
description: "Author deleted successfully",
},
403: {
content: { "application/json": { schema: ForbiddenSchema } },
description: "Public API key used for write operation",
},
404: {
content: { "application/json": { schema: NotFoundSchema } },
description: "Author not found",
},
500: {
content: { "application/json": { schema: ServerErrorSchema } },
description: "Server error",
},
},
});
authors.openapi(deleteAuthorRoute, async (c) => {
try {
const db = createDbClient(c.env);
const workspaceId = requireWorkspaceId(c);
const cache = createCacheClient(c.env.REDIS_URL, c.env.REDIS_TOKEN);
const { identifier } = c.req.valid("param");
const existingAuthor = await db.author.findFirst({
where: {
workspaceId,
OR: [{ id: identifier }, { slug: identifier }],
},
include: {
socials: {
select: { url: true, platform: true },
},
},
});
if (!existingAuthor) {
return c.json(
{
error: "Author not found",
message: "The requested author does not exist",
},
404 as const
);
}
await db.author.delete({
where: { id: existingAuthor.id },
});
c.executionCtx.waitUntil(cache.invalidateResource(workspaceId, "authors"));
c.executionCtx.waitUntil(cache.invalidateResource(workspaceId, "posts"));
const apiKeyId = c.get("apiKeyId");
c.executionCtx.waitUntil(
emitEvent(db, c.env.EVENT_QUEUE, {
type: "author_deleted",
workspaceId,
resourceType: "author",
resourceId: existingAuthor.id,
actorType: "api_key",
actorId: apiKeyId,
payload: toAuthorPayload(existingAuthor),
}).catch((error) => {
console.error("[authors.delete] Failed to emit author_deleted:", error);
})
);
return c.json({ id: existingAuthor.id }, 200 as const);
} catch (error) {
console.error("Error deleting author:", error);
return c.json(
{
error: "Failed to delete author",
message: "An unexpected error occurred",
},
500 as const
);
}
});
export default authors;
================================================
FILE: apps/api/src/routes/cache.ts
================================================
import { Hono } from "hono";
import { createCacheClient } from "@/lib/cache";
import type { Env } from "@/types/env";
import { SystemCacheInvalidateSchema } from "@/validations/misc";
const cacheInvalidate = new Hono<{ Bindings: Env }>();
cacheInvalidate.post("/", async (c) => {
const cache = createCacheClient(c.env.REDIS_URL, c.env.REDIS_TOKEN);
try {
const rawBody = await c.req.json();
const validation = SystemCacheInvalidateSchema.safeParse(rawBody);
if (!validation.success) {
return c.json(
{
error: "Invalid request body",
details: validation.error.issues.map((err) => ({
field: err.path.join("."),
message: err.message,
})),
},
400
);
}
const { workspaceId, resource } = validation.data;
let invalidatedCount: number;
if (resource === "usage") {
const redis = new (await import("@upstash/redis/cloudflare")).Redis({
url: c.env.REDIS_URL,
token: c.env.REDIS_TOKEN,
});
const deleted = await redis.del(`usage:meta:${workspaceId}`);
return c.json({
success: true,
message: `Invalidated usage cache${deleted ? "" : " (was not cached)"}`,
workspaceId,
resource,
});
}
if (resource) {
invalidatedCount = await cache.invalidateResource(workspaceId, resource);
return c.json({
success: true,
message: `Invalidated ${invalidatedCount} cache entries for ${resource}`,
workspaceId,
resource,
});
}
invalidatedCount = await cache.invalidateWorkspace(workspaceId);
return c.json({
success: true,
message: `Invalidated ${invalidatedCount} cache entries for workspace`,
workspaceId,
});
} catch (error) {
console.error("[Cache] Invalidation error:", error);
return c.json(
{
error: "Failed to invalidate cache",
message: error instanceof Error ? error.message : "Unknown error",
},
500
);
}
});
export default cacheInvalidate;
================================================
FILE: apps/api/src/routes/categories.ts
================================================
import { createRoute, OpenAPIHono, z } from "@hono/zod-openapi";
import { toCategoryPayload, withChanges } from "@marble/events";
import { cacheKey, createCacheClient, hashQueryParams } from "@/lib/cache";
import { createDbClient } from "@/lib/db";
import { emitEvent } from "@/lib/events";
import { requireWorkspaceId } from "@/lib/workspace";
import {
CategoriesListResponseSchema,
CategoryResponseSchema,
CreateCategoryBodySchema,
CreateCategoryResponseSchema,
UpdateCategoryBodySchema,
} from "@/schemas/categories";
import {
ConflictSchema,
DeleteResponseSchema,
ErrorSchema,
ForbiddenSchema,
LimitQuerySchema,
NotFoundSchema,
PageNotFoundSchema,
PageQuerySchema,
ServerErrorSchema,
} from "@/schemas/common";
import type { ApiKeyApp } from "@/types/env";
const categories = new OpenAPIHono<ApiKeyApp>();
const CategoriesQuerySchema = z.object({
limit: LimitQuerySchema,
page: PageQuerySchema,
});
const CategoryParamsSchema = z.object({
identifier: z.string().openapi({
param: { name: "identifier", in: "path" },
example: "technology",
description: "Category ID or slug",
}),
});
const listCategoriesRoute = createRoute({
method: "get",
path: "/",
tags: ["Categories"],
summary: "List categories",
description: "Get a paginated list of categories",
request: {
query: CategoriesQuerySchema,
},
responses: {
200: {
content: { "application/json": { schema: CategoriesListResponseSchema } },
description: "Paginated list of categories",
},
400: {
content: {
"application/json": {
schema: z.union([ErrorSchema, PageNotFoundSchema]),
},
},
description: "Invalid query parameters or page number",
},
500: {
content: { "application/json": { schema: ServerErrorSchema } },
description: "Server error",
},
},
});
const getCategoryRoute = createRoute({
method: "get",
path: "/{identifier}",
tags: ["Categories"],
summary: "Get category",
description: "Get a single category by ID or slug",
request: {
params: CategoryParamsSchema,
},
responses: {
200: {
content: { "application/json": { schema: CategoryResponseSchema } },
description: "The requested category",
},
404: {
content: { "application/json": { schema: NotFoundSchema } },
description: "Category not found",
},
500: {
content: { "application/json": { schema: ServerErrorSchema } },
description: "Server error",
},
},
});
const createCategoryRoute = createRoute({
method: "post",
path: "/",
tags: ["Categories"],
summary: "Create category",
description: "Create a new category. Requires a private API key.",
request: {
body: {
content: { "application/json": { schema: CreateCategoryBodySchema } },
required: true,
},
},
responses: {
201: {
content: {
"application/json": { schema: CreateCategoryResponseSchema },
},
description: "Category created successfully",
},
400: {
content: { "application/json": { schema: ErrorSchema } },
description: "Invalid request body",
},
403: {
content: { "application/json": { schema: ForbiddenSchema } },
description: "Public API key used for write operation",
},
409: {
content: { "application/json": { schema: ConflictSchema } },
description: "Category with this slug already exists",
},
500: {
content: { "application/json": { schema: ServerErrorSchema } },
description: "Server error",
},
},
});
categories.openapi(listCategoriesRoute, async (c) => {
try {
const workspaceId = requireWorkspaceId(c);
const db = createDbClient(c.env);
const cache = createCacheClient(c.env.REDIS_URL, c.env.REDIS_TOKEN);
const { limit, page } = c.req.valid("query");
// Generate cache key for count (exclude page - it doesn't affect count)
const countCacheKey = cacheKey(
workspaceId,
"categories",
"list",
hashQueryParams({ limit }),
"count"
);
// Cache count query separately (1 hour TTL, invalidated with posts)
const totalCategories = await cache.getOrSetCount(countCacheKey, () =>
db.category.count({
where: { workspaceId },
})
);
// Generate cache key for data (includes page)
const listCacheKey = cacheKey(
workspaceId,
"categories",
"list",
hashQueryParams({ page, limit })
);
const totalPages = Math.ceil(totalCategories / limit);
const prevPage = page > 1 ? page - 1 : null;
const nextPage = page < totalPages ? page + 1 : null;
const categoriesToSkip = limit ? (page - 1) * limit : 0;
// Validate page number
if (page > totalPages && totalCategories > 0) {
return c.json(
{
error: "Invalid page number" as const,
details: {
message: `Page ${page} does not exist.`,
totalPages,
requestedPage: page,
},
},
400 as const
);
}
const categoriesList = await cache.getOrSet(listCacheKey, () =>
db.category.findMany({
where: {
workspaceId,
},
select: {
id: true,
name: true,
slug: true,
description: true,
_count: {
select: {
posts: {
where: {
status: "published",
},
},
},
},
},
take: limit,
skip: categoriesToSkip,
})
);
const transformedCategories = categoriesList.map((category) => {
const { _count, ...rest } = category;
return {
...rest,
count: _count,
};
});
return c.json(
{
categories: transformedCategories,
pagination: {
limit,
currentPage: page,
nextPage,
previousPage: prevPage,
totalPages,
totalItems: totalCategories,
},
},
200 as const
);
} catch (error) {
console.error("Error fetching categories:", error);
return c.json({ error: "Failed to fetch categories" }, 500 as const);
}
});
categories.openapi(getCategoryRoute, async (c) => {
try {
const workspaceId = requireWorkspaceId(c);
const { identifier } = c.req.valid("param");
const db = createDbClient(c.env);
const cache = createCacheClient(c.env.REDIS_URL, c.env.REDIS_TOKEN);
// Cache by identifier (slug or id)
const singleCacheKey = cacheKey(workspaceId, "categories", identifier);
const category = await cache.getOrSet(singleCacheKey, () =>
db.category.findFirst({
where: {
workspaceId,
OR: [{ id: identifier }, { slug: identifier }],
},
select: {
id: true,
name: true,
slug: true,
description: true,
_count: {
select: {
posts: {
where: {
status: "published",
},
},
},
},
},
})
);
if (!category) {
return c.json(
{
error: "Category not found",
message: "The requested category does not exist",
},
404 as const
);
}
// Transform _count to count
const { _count, ...rest } = category;
const transformedCategory = {
...rest,
count: _count,
};
return c.json({ category: transformedCategory }, 200 as const);
} catch (error) {
console.error("Error fetching category:", error);
return c.json({ error: "Failed to fetch category" }, 500 as const);
}
});
categories.openapi(createCategoryRoute, async (c) => {
try {
const workspaceId = requireWorkspaceId(c);
const db = createDbClient(c.env);
const cache = createCacheClient(c.env.REDIS_URL, c.env.REDIS_TOKEN);
const body = c.req.valid("json");
// Check for slug uniqueness within workspace
const existingCategory = await db.category.findFirst({
where: {
slug: body.slug,
workspaceId,
},
});
if (existingCategory) {
return c.json(
{
error: "Slug already in use",
message: "A category with this slug already exists in this workspace",
},
409 as const
);
}
const categoryCreated = await db.category.create({
data: {
name: body.name,
slug: body.slug,
description: body.description ?? null,
workspaceId,
},
select: {
id: true,
name: true,
slug: true,
description: true,
},
});
// Invalidate cache for categories and posts
c.executionCtx.waitUntil(
cache.invalidateResource(workspaceId, "categories")
);
c.executionCtx.waitUntil(cache.invalidateResource(workspaceId, "posts"));
const apiKeyId = c.get("apiKeyId");
c.executionCtx.waitUntil(
emitEvent(db, c.env.EVENT_QUEUE, {
type: "category_created",
workspaceId,
resourceType: "category",
resourceId: categoryCreated.id,
actorType: "api_key",
actorId: apiKeyId,
payload: toCategoryPayload(categoryCreated),
}).catch((error) => {
console.error(
"[categories.create] Failed to emit category_created:",
error
);
})
);
return c.json({ category: categoryCreated }, 201 as const);
} catch (error) {
console.error("Error creating category:", error);
return c.json(
{
error: "Failed to create category",
message: "An unexpected error occurred",
},
500 as const
);
}
});
const updateCategoryRoute = createRoute({
method: "patch",
path: "/{identifier}",
tags: ["Categories"],
summary: "Update category",
description:
"Update an existing category by ID or slug. Requires a private API key.",
request: {
params: CategoryParamsSchema,
body: {
content: { "application/json": { schema: UpdateCategoryBodySchema } },
required: true,
},
},
responses: {
200: {
content: {
"application/json": { schema: CreateCategoryResponseSchema },
},
description: "Category updated successfully",
},
400: {
content: { "application/json": { schema: ErrorSchema } },
description: "Invalid request body",
},
403: {
content: { "application/json": { schema: ForbiddenSchema } },
description: "Public API key used for write operation",
},
404: {
content: { "application/json": { schema: NotFoundSchema } },
description: "Category not found",
},
409: {
content: { "application/json": { schema: ConflictSchema } },
description: "Category with this slug already exists",
},
500: {
content: { "application/json": { schema: ServerErrorSchema } },
description: "Server error",
},
},
});
const deleteCategoryRoute = createRoute({
method: "delete",
path: "/{identifier}",
tags: ["Categories"],
summary: "Delete category",
description:
"Delete a category by ID or slug. Requires a private API key. Cannot delete a category that has posts assigned to it.",
request: {
params: CategoryParamsSchema,
},
responses: {
200: {
content: { "application/json": { schema: DeleteResponseSchema } },
description: "Category deleted successfully",
},
400: {
content: { "application/json": { schema: ErrorSchema } },
description: "Category has posts assigned to it",
},
403: {
content: { "application/json": { schema: ForbiddenSchema } },
description: "Public API key used for write operation",
},
404: {
content: { "application/json": { schema: NotFoundSchema } },
description: "Category not found",
},
500: {
content: { "application/json": { schema: ServerErrorSchema } },
description: "Server error",
},
},
});
categories.openapi(updateCategoryRoute, async (c) => {
try {
const workspaceId = requireWorkspaceId(c);
const db = createDbClient(c.env);
const cache = createCacheClient(c.env.REDIS_URL, c.env.REDIS_TOKEN);
const { identifier } = c.req.valid("param");
const body = c.req.valid("json");
const existingCategory = await db.category.findFirst({
where: {
workspaceId,
OR: [{ id: identifier }, { slug: identifier }],
},
});
if (!existingCategory) {
return c.json(
{
error: "Category not found",
message: "The requested category does not exist",
},
404 as const
);
}
// If slug is being changed, check uniqueness
if (body.slug && body.slug !== existingCategory.slug) {
const slugConflict = await db.category.findFirst({
where: {
slug: body.slug,
workspaceId,
id: { not: existingCategory.id },
},
});
if (slugConflict) {
return c.json(
{
error: "Slug already in use",
message:
"A category with this slug already exists in this workspace",
},
409 as const
);
}
}
const categoryUpdated = await db.category.update({
where: { id: existingCategory.id },
data: {
...(body.name !== undefined && { name: body.name }),
...(body.slug !== undefined && { slug: body.slug }),
...(body.description !== undefined && {
description: body.description,
}),
},
select: {
id: true,
name: true,
slug: true,
description: true,
},
});
c.executionCtx.waitUntil(
cache.invalidateResource(workspaceId, "categories")
);
c.executionCtx.waitUntil(cache.invalidateResource(workspaceId, "posts"));
const apiKeyId = c.get("apiKeyId");
c.executionCtx.waitUntil(
emitEvent(db, c.env.EVENT_QUEUE, {
type: "category_updated",
workspaceId,
resourceType: "category",
resourceId: categoryUpdated.id,
actorType: "api_key",
actorId: apiKeyId,
payload: withChanges(
toCategoryPayload(categoryUpdated),
Object.keys(body)
),
}).catch((error) => {
console.error(
"[categories.update] Failed to emit category_updated:",
error
);
})
);
return c.json({ category: categoryUpdated }, 200 as const);
} catch (error) {
console.error("Error updating category:", error);
return c.json(
{
error: "Failed to update category",
message: "An unexpected error occurred",
},
500 as const
);
}
});
categories.openapi(deleteCategoryRoute, async (c) => {
try {
const workspaceId = requireWorkspaceId(c);
const db = createDbClient(c.env);
const cache = createCacheClient(c.env.REDIS_URL, c.env.REDIS_TOKEN);
const { identifier } = c.req.valid("param");
const existingCategory = await db.category.findFirst({
where: {
workspaceId,
OR: [{ id: identifier }, { slug: identifier }],
},
include: {
_count: { select: { posts: true } },
},
});
if (!existingCategory) {
return c.json(
{
error: "Category not found",
message: "The requested category does not exist",
},
404 as const
);
}
// Prevent deleting a category that has posts
if (existingCategory._count.posts > 0) {
return c.json(
{
error: "Category has posts",
message: `This category has ${existingCategory._count.posts} post(s) assigned to it. Reassign or delete them before deleting this category.`,
},
400 as const
);
}
await db.category.delete({
where: { id: existingCategory.id },
});
c.executionCtx.waitUntil(
cache.invalidateResource(workspaceId, "categories")
);
c.executionCtx.waitUntil(cache.invalidateResource(workspaceId, "posts"));
const apiKeyId = c.get("apiKeyId");
c.executionCtx.waitUntil(
emitEvent(db, c.env.EVENT_QUEUE, {
type: "category_deleted",
workspaceId,
resourceType: "category",
resourceId: existingCategory.id,
actorType: "api_key",
actorId: apiKeyId,
payload: toCategoryPayload(existingCategory),
}).catch((error) => {
console.error(
"[categories.delete] Failed to emit category_deleted:",
error
);
})
);
return c.json({ id: existingCategory.id }, 200 as const);
} catch (error) {
console.error("Error deleting category:", error);
return c.json(
{
error: "Failed to delete category",
message: "An unexpected error occurred",
},
500 as const
);
}
});
export default categories;
================================================
FILE: apps/api/src/routes/events.ts
================================================
import { Hono } from "hono";
import { createDbClient } from "@/lib/db";
import type { Env } from "@/types/env";
import { InternalEventSchema } from "@/validations/misc";
const events = new Hono<{ Bindings: Env }>();
events.post("/", async (c) => {
let rawBody: unknown;
try {
rawBody = await c.req.json();
} catch {
return c.json({ error: "Invalid JSON body" }, 400);
}
const validation = InternalEventSchema.safeParse(rawBody);
if (!validation.success) {
return c.json(
{
error: "Invalid event payload",
details: validation.error.issues.map((err) => ({
field: err.path.join("."),
message: err.message,
})),
},
400
);
}
const body = validation.data;
const db = createDbClient(c.env);
const workspace = await db.organization.findUnique({
where: { id: body.workspaceId },
select: { id: true },
});
if (!workspace) {
return c.json({ error: "Workspace not found" }, 404);
}
try {
const event = await db.workspaceEvent.create({
data: {
type: body.type,
workspaceId: body.workspaceId,
source: body.source,
resourceType: body.resourceType,
resourceId: body.resourceId,
actorType: body.actorType,
actorId: body.actorId,
payload: body.payload ?? {},
},
});
await c.env.EVENT_QUEUE.send({
eventId: event.id,
targetWebhookEndpointId: body.targetWebhookEndpointId,
isTest: body.isTest,
});
return c.json({ ok: true, eventId: event.id });
} catch (error) {
console.error("[InternalEvents] Failed to create event:", error);
return c.json(
{
error: "Failed to create event",
message: error instanceof Error ? error.message : "Unknown error",
},
500
);
}
});
export default events;
================================================
FILE: apps/api/src/routes/invalidate.ts
================================================
import { Redis } from "@upstash/redis/cloudflare";
import { Hono } from "hono";
import { createCacheClient } from "@/lib/cache";
import type { ApiKeyApp } from "@/types/env";
import { CacheInvalidateSchema } from "@/validations/misc";
const invalidate = new Hono<ApiKeyApp>();
/**
* Cache invalidation endpoint
* Allows CMS or admin to invalidate cached data when content changes
*
* POST /v1/cache/invalidate
* - Invalidates all cache for workspace if no resource specified
* - Invalidates specific resource cache if resource is provided
*
* Requires API key authentication (private key recommended)
*/
invalidate.post("/", async (c) => {
const workspaceId = c.get("workspaceId");
if (!workspaceId) {
return c.json({ error: "Workspace ID is required" }, 400);
}
const cache = createCacheClient(c.env.REDIS_URL, c.env.REDIS_TOKEN);
try {
const rawBody = await c.req.json();
const validation = CacheInvalidateSchema.safeParse(rawBody);
if (!validation.success) {
return c.json(
{
error: "Invalid request body",
details: validation.error.issues.map((err) => ({
field: err.path.join("."),
message: err.message,
})),
},
400
);
}
const { resource } = validation.data;
let invalidatedCount: number;
if (resource === "usage") {
const redis = new Redis({
url: c.env.REDIS_URL,
token: c.env.REDIS_TOKEN,
});
const deleted = await redis.del(`usage:meta:${workspaceId}`);
return c.json({
success: true,
message: `Invalidated usage cache${deleted ? "" : " (was not cached)"}`,
workspaceId,
resource,
});
}
if (resource) {
// Invalidate specific resource
invalidatedCount = await cache.invalidateResource(workspaceId, resource);
return c.json({
success: true,
message: `Invalidated ${invalidatedCount} cache entries for ${resource}`,
workspaceId,
resource,
});
}
// Invalidate all workspace cache
invalidatedCount = await cache.invalidateWorkspace(workspaceId);
return c.json({
success: true,
message: `Invalidated ${invalidatedCount} cache entries for workspace`,
workspaceId,
});
} catch (error) {
console.error("[Cache] Invalidation error:", error);
return c.json(
{
error: "Failed to invalidate cache",
message: error instanceof Error ? error.message : "Unknown error",
},
500
);
}
});
export default invalidate;
================================================
FILE: apps/api/src/routes/media.ts
================================================
import { createRoute, OpenAPIHono } from "@hono/zod-openapi";
import { toMediaPayload, withChanges } from "@marble/events";
import { cacheKey, createCacheClient, hashQueryParams } from "@/lib/cache";
import { ALLOWED_MEDIA_MIME_TYPES, MAX_UPLOAD_SIZE } from "@/lib/constants";
import { createDbClient } from "@/lib/db";
import { emitEvent } from "@/lib/events";
import {
extensionFromFile,
getImageDimensions,
getMediaType,
objectKeyFromUrl,
publicUrl,
serializeMedia,
} from "@/lib/media";
import { requireWorkspaceId } from "@/lib/workspace";
import {
DeleteResponseSchema,
ErrorSchema,
ForbiddenSchema,
NotFoundSchema,
PageNotFoundSchema,
ServerErrorSchema,
} from "@/schemas/common";
import {
MediaListResponseSchema,
MediaParamsSchema,
MediaQuerySchema,
MediaResponseSchema,
UpdateMediaBodySchema,
UploadMediaBodySchema,
} from "@/schemas/media";
import type { ApiKeyApp } from "@/types/env";
const media = new OpenAPIHono<ApiKeyApp>();
function isUploadedFile(value: unknown): value is File {
return value !== null && typeof value !== "string";
}
const listMediaRoute = createRoute({
method: "get",
path: "/",
tags: ["Media"],
summary: "List media assets",
description: "Retrieve media assets for the authenticated workspace.",
request: { query: MediaQuerySchema },
responses: {
200: {
content: { "application/json": { schema: MediaListResponseSchema } },
description: "Media assets retrieved successfully",
},
400: {
content: { "application/json": { schema: ErrorSchema } },
description: "Invalid query parameters",
},
404: {
content: { "application/json": { schema: PageNotFoundSchema } },
description: "Page number does not exist",
},
500: {
content: { "application/json": { schema: ServerErrorSchema } },
description: "Server error",
},
},
});
const getMediaRoute = createRoute({
method: "get",
path: "/{id}",
tags: ["Media"],
summary: "Get media asset",
description: "Retrieve a single media asset by ID.",
request: { params: MediaParamsSchema },
responses: {
200: {
content: { "application/json": { schema: MediaResponseSchema } },
description: "Media asset retrieved successfully",
},
404: {
content: { "application/json": { schema: NotFoundSchema } },
description: "Media asset not found",
},
500: {
content: { "application/json": { schema: ServerErrorSchema } },
description: "Server error",
},
},
});
const updateMediaRoute = createRoute({
method: "patch",
path: "/{id}",
tags: ["Media"],
summary: "Update media asset",
description: "Update media asset metadata. Requires a private API key.",
request: {
params: MediaParamsSchema,
body: {
content: { "application/json": { schema: UpdateMediaBodySchema } },
required: true,
},
},
responses: {
200: {
content: { "application/json": { schema: MediaResponseSchema } },
description: "Media asset updated successfully",
},
400: {
content: { "application/json": { schema: ErrorSchema } },
description: "Invalid request body",
},
403: {
content: { "application/json": { schema: ForbiddenSchema } },
description: "Public API key used for write operation",
},
404: {
content: { "application/json": { schema: NotFoundSchema } },
description: "Media asset not found",
},
500: {
content: { "application/json": { schema: ServerErrorSchema } },
description: "Server error",
},
},
});
const deleteMediaRoute = createRoute({
method: "delete",
path: "/{id}",
tags: ["Media"],
summary: "Delete media asset",
description:
"Delete a media asset and its R2 object. Requires a private API key.",
request: { params: MediaParamsSchema },
responses: {
200: {
content: { "application/json": { schema: DeleteResponseSchema } },
description: "Media asset deleted successfully",
},
403: {
content: { "application/json": { schema: ForbiddenSchema } },
description: "Public API key used for write operation",
},
404: {
content: { "application/json": { schema: NotFoundSchema } },
description: "Media asset not found",
},
500: {
content: { "application/json": { schema: ServerErrorSchema } },
description: "Server error",
},
},
});
const uploadMediaRoute = createRoute({
method: "post",
path: "/upload",
tags: ["Media"],
summary: "Upload media asset",
description:
"Upload a media file and create a media asset. Requires a private API key. Maximum file size is 5 MiB.",
request: {
body: {
content: { "multipart/form-data": { schema: UploadMediaBodySchema } },
required: true,
},
},
responses: {
201: {
content: { "application/json": { schema: MediaResponseSchema } },
description: "Media asset uploaded successfully",
},
400: {
content: { "application/json": { schema: ErrorSchema } },
description: "Invalid upload request",
},
403: {
content: { "application/json": { schema: ForbiddenSchema } },
description: "Public API key used for write operation",
},
413: {
content: { "application/json": { schema: ErrorSchema } },
description: "File exceeds the upload size limit",
},
500: {
content: { "application/json": { schema: ServerErrorSchema } },
description: "Server error",
},
},
});
media.openapi(listMediaRoute, async (c) => {
try {
const db = createDbClient(c.env);
const workspaceId = requireWorkspaceId(c);
const cache = createCacheClient(c.env.REDIS_URL, c.env.REDIS_TOKEN);
const query = c.req.valid("query");
const { limit, page, order, type } = query;
const skip = (page - 1) * limit;
const where = {
workspaceId,
...(type ? { type } : {}),
...(query.query
? {
OR: [
{ name: { contains: query.query, mode: "insensitive" as const } },
{ alt: { contains: query.query, mode: "insensitive" as const } },
{ url: { contains: query.query, mode: "insensitive" as const } },
{
mimeType: {
contains: query.query,
mode: "insensitive" as const,
},
},
],
}
: {}),
};
const key = cacheKey(workspaceId, "media", "list", hashQueryParams(query));
const response = await cache.getOrSet(key, async () => {
const [items, totalItems] = await Promise.all([
db.media.findMany({
where,
orderBy: { createdAt: order },
skip,
take: limit,
}),
db.media.count({ where }),
]);
const totalPages = Math.ceil(totalItems / limit);
return {
media: items.map(serializeMedia),
pagination: {
limit,
currentPage: page,
nextPage: page < totalPages ? page + 1 : null,
previousPage: page > 1 ? page - 1 : null,
totalPages,
totalItems,
},
};
});
if (
page > response.pagination.totalPages &&
response.pagination.totalItems > 0
) {
return c.json(
{
error: "Invalid page number" as const,
details: {
message: `Page ${page} does not exist.`,
totalPages: response.pagination.totalPages,
requestedPage: page,
},
},
404 as const
);
}
return c.json(response, 200 as const);
} catch (error) {
console.error("Error fetching media:", error);
return c.json(
{
error: "Failed to fetch media",
message: "An unexpected error occurred",
},
500 as const
);
}
});
media.openapi(getMediaRoute, async (c) => {
try {
const db = createDbClient(c.env);
const workspaceId = requireWorkspaceId(c);
const { id } = c.req.valid("param");
const item = await db.media.findFirst({ where: { id, workspaceId } });
if (!item) {
return c.json(
{
error: "Media not found",
message: "The requested media asset does not exist",
},
404 as const
);
}
return c.json({ media: serializeMedia(item) }, 200 as const);
} catch (error) {
console.error("Error fetching media asset:", error);
return c.json(
{
error: "Failed to fetch media",
message: "An unexpected error occurred",
},
500 as const
);
}
});
media.openapi(updateMediaRoute, async (c) => {
try {
const db = createDbClient(c.env);
const workspaceId = requireWorkspaceId(c);
const cache = createCacheClient(c.env.REDIS_URL, c.env.REDIS_TOKEN);
const { id } = c.req.valid("param");
const body = c.req.valid("json");
const existing = await db.media.findFirst({ where: { id, workspaceId } });
if (!existing) {
return c.json(
{
error: "Media not found",
message: "The requested media asset does not exist",
},
404 as const
);
}
const updated = await db.media.update({
where: { id },
data: {
...(body.name !== undefined ? { name: body.name } : {}),
...(body.alt !== undefined ? { alt: body.alt } : {}),
},
});
c.executionCtx.waitUntil(cache.invalidateResource(workspaceId, "media"));
c.executionCtx.waitUntil(
emitEvent(db, c.env.EVENT_QUEUE, {
type: "media_updated",
workspaceId,
resourceType: "media",
resourceId: updated.id,
actorType: "api_key",
actorId: c.get("apiKeyId"),
payload: withChanges(toMediaPayload(updated), Object.keys(body)),
}).catch((error) => {
console.error("[media.update] Failed to emit media_updated:", error);
})
);
return c.json({ media: serializeMedia(updated) }, 200 as const);
} catch (error) {
console.error("Error updating media asset:", error);
return c.json(
{
error: "Failed to update media",
message: "An unexpected error occurred",
},
500 as const
);
}
});
media.openapi(deleteMediaRoute, async (c) => {
try {
const db = createDbClient(c.env);
const workspaceId = requireWorkspaceId(c);
const cache = createCacheClient(c.env.REDIS_URL, c.env.REDIS_TOKEN);
const { id } = c.req.valid("param");
const existing = await db.media.findFirst({ where: { id, workspaceId } });
if (!existing) {
return c.json(
{
error: "Media not found",
message: "The requested media asset does not exist",
},
404 as const
);
}
await db.media.delete({ where: { id } });
const key = objectKeyFromUrl(existing.url);
if (key) {
c.executionCtx.waitUntil(c.env.STORAGE.delete(key));
}
c.executionCtx.waitUntil(cache.invalidateResource(workspaceId, "media"));
c.executionCtx.waitUntil(
emitEvent(db, c.env.EVENT_QUEUE, {
type: "media_deleted",
workspaceId,
resourceType: "media",
resourceId: id,
actorType: "api_key",
actorId: c.get("apiKeyId"),
payload: toMediaPayload(existing),
}).catch((error) => {
console.error("[media.delete] Failed to emit media_deleted:", error);
})
);
return c.json({ id }, 200 as const);
} catch (error) {
console.error("Error deleting media asset:", error);
return c.json(
{
error: "Failed to delete media",
message: "An unexpected error occurred",
},
500 as const
);
}
});
media.openapi(uploadMediaRoute, async (c) => {
try {
const db = createDbClient(c.env);
const workspaceId = requireWorkspaceId(c);
const cache = createCacheClient(c.env.REDIS_URL, c.env.REDIS_TOKEN);
const formData = await c.req.formData();
const file = formData.get("file");
if (!isUploadedFile(file)) {
return c.json(
{
error: "Invalid upload request",
message: "A file field is required",
},
400 as const
);
}
if (file.size > MAX_UPLOAD_SIZE) {
return c.json(
{
error: "File too large",
message: `Media uploads are limited to ${MAX_UPLOAD_SIZE / 1024 / 1024} MiB`,
},
413 as const
);
}
const contentType = file.type || "application/octet-stream";
if (
!(ALLOWED_MEDIA_MIME_TYPES as readonly string[]).includes(contentType)
gitextract_4dlc520n/ ├── .cursor/ │ └── rules/ │ ├── mintlify.mdc │ └── ultracite.mdc ├── .dockerignore ├── .github/ │ ├── CONTRIBUTING.md │ ├── FUNDING.yml │ ├── ISSUE_TEMPLATE/ │ │ ├── bug_report.md │ │ └── feature_request.md │ ├── pull_request_template.md │ └── workflows/ │ ├── code-quality.yml │ └── tests.yml ├── .gitignore ├── .husky/ │ └── commit-msg ├── .npmrc ├── .vscode/ │ ├── extensions.json │ └── settings.json ├── CODE_OF_CONDUCT.md ├── LICENSE ├── README.md ├── apps/ │ ├── api/ │ │ ├── .gitignore │ │ ├── README.md │ │ ├── package.json │ │ ├── src/ │ │ │ ├── app.ts │ │ │ ├── index.ts │ │ │ ├── lib/ │ │ │ │ ├── cache.ts │ │ │ │ ├── constants.ts │ │ │ │ ├── crypto.ts │ │ │ │ ├── db.ts │ │ │ │ ├── events.ts │ │ │ │ ├── media.ts │ │ │ │ ├── polar.ts │ │ │ │ ├── posts.ts │ │ │ │ ├── redis.ts │ │ │ │ ├── sanitize.ts │ │ │ │ ├── usage.ts │ │ │ │ └── workspace.ts │ │ │ ├── middleware/ │ │ │ │ ├── analytics.ts │ │ │ │ ├── authorization.ts │ │ │ │ ├── cache.ts │ │ │ │ ├── key-authorization.ts │ │ │ │ ├── legacy-analytics.ts │ │ │ │ ├── ratelimit.ts │ │ │ │ └── system.ts │ │ │ ├── routes/ │ │ │ │ ├── authors.ts │ │ │ │ ├── cache.ts │ │ │ │ ├── categories.ts │ │ │ │ ├── events.ts │ │ │ │ ├── invalidate.ts │ │ │ │ ├── media.ts │ │ │ │ ├── posts.ts │ │ │ │ └── tags.ts │ │ │ ├── schemas/ │ │ │ │ ├── authors.ts │ │ │ │ ├── categories.ts │ │ │ │ ├── common.ts │ │ │ │ ├── media.ts │ │ │ │ ├── posts.ts │ │ │ │ └── tags.ts │ │ │ ├── types/ │ │ │ │ └── env.ts │ │ │ └── validations/ │ │ │ ├── authors.ts │ │ │ ├── categories.ts │ │ │ ├── json.ts │ │ │ ├── misc.ts │ │ │ ├── posts.ts │ │ │ └── tags.ts │ │ ├── tsconfig.json │ │ └── wrangler.jsonc │ ├── cms/ │ │ ├── .env.example │ │ ├── .gitignore │ │ ├── README.md │ │ ├── components.json │ │ ├── next.config.ts │ │ ├── package.json │ │ ├── postcss.config.mjs │ │ ├── public/ │ │ │ └── manifest.json │ │ ├── src/ │ │ │ ├── app/ │ │ │ │ ├── (auth)/ │ │ │ │ │ ├── join/ │ │ │ │ │ │ └── [id]/ │ │ │ │ │ │ ├── page-client.tsx │ │ │ │ │ │ └── page.tsx │ │ │ │ │ ├── layout.tsx │ │ │ │ │ ├── login/ │ │ │ │ │ │ └── page.tsx │ │ │ │ │ ├── new/ │ │ │ │ │ │ ├── page-client.tsx │ │ │ │ │ │ └── page.tsx │ │ │ │ │ ├── register/ │ │ │ │ │ │ └── page.tsx │ │ │ │ │ ├── reset/ │ │ │ │ │ │ └── page.tsx │ │ │ │ │ └── verify/ │ │ │ │ │ └── page.tsx │ │ │ │ ├── (main)/ │ │ │ │ │ ├── [workspace]/ │ │ │ │ │ │ ├── (dashboard)/ │ │ │ │ │ │ │ ├── (home)/ │ │ │ │ │ │ │ │ ├── page-client.tsx │ │ │ │ │ │ │ │ └── page.tsx │ │ │ │ │ │ │ ├── authors/ │ │ │ │ │ │ │ │ ├── page-client.tsx │ │ │ │ │ │ │ │ └── page.tsx │ │ │ │ │ │ │ ├── categories/ │ │ │ │ │ │ │ │ ├── page-client.tsx │ │ │ │ │ │ │ │ └── page.tsx │ │ │ │ │ │ │ ├── layout.tsx │ │ │ │ │ │ │ ├── loading.tsx │ │ │ │ │ │ │ ├── media/ │ │ │ │ │ │ │ │ ├── [id]/ │ │ │ │ │ │ │ │ │ ├── page-client.tsx │ │ │ │ │ │ │ │ │ └── page.tsx │ │ │ │ │ │ │ │ ├── page-client.tsx │ │ │ │ │ │ │ │ └── page.tsx │ │ │ │ │ │ │ ├── posts/ │ │ │ │ │ │ │ │ ├── page-client.tsx │ │ │ │ │ │ │ │ └── page.tsx │ │ │ │ │ │ │ ├── settings/ │ │ │ │ │ │ │ │ ├── account/ │ │ │ │ │ │ │ │ │ ├── page-client.tsx │ │ │ │ │ │ │ │ │ └── page.tsx │ │ │ │ │ │ │ │ ├── appearance/ │ │ │ │ │ │ │ │ │ ├── page-client.tsx │ │ │ │ │ │ │ │ │ └── page.tsx │ │ │ │ │ │ │ │ ├── billing/ │ │ │ │ │ │ │ │ │ ├── page-client.tsx │ │ │ │ │ │ │ │ │ └── page.tsx │ │ │ │ │ │ │ │ ├── fields/ │ │ │ │ │ │ │ │ │ ├── page-client.tsx │ │ │ │ │ │ │ │ │ └── page.tsx │ │ │ │ │ │ │ │ ├── general/ │ │ │ │ │ │ │ │ │ ├── page-client.tsx │ │ │ │ │ │ │ │ │ └── page.tsx │ │ │ │ │ │ │ │ ├── keys/ │ │ │ │ │ │ │ │ │ ├── page-client.tsx │ │ │ │ │ │ │ │ │ └── page.tsx │ │ │ │ │ │ │ │ ├── members/ │ │ │ │ │ │ │ │ │ ├── page-client.tsx │ │ │ │ │ │ │ │ │ └── page.tsx │ │ │ │ │ │ │ │ ├── notifications/ │ │ │ │ │ │ │ │ │ ├── page-client.tsx │ │ │ │ │ │ │ │ │ └── page.tsx │ │ │ │ │ │ │ │ └── webhooks/ │ │ │ │ │ │ │ │ ├── page-client.tsx │ │ │ │ │ │ │ │ └── page.tsx │ │ │ │ │ │ │ └── tags/ │ │ │ │ │ │ │ ├── page-client.tsx │ │ │ │ │ │ │ └── page.tsx │ │ │ │ │ │ ├── (editor)/ │ │ │ │ │ │ │ ├── editor/ │ │ │ │ │ │ │ │ └── p/ │ │ │ │ │ │ │ │ ├── [id]/ │ │ │ │ │ │ │ │ │ ├── page-client.tsx │ │ │ │ │ │ │ │ │ └── page.tsx │ │ │ │ │ │ │ │ └── new/ │ │ │ │ │ │ │ │ ├── page-client.tsx │ │ │ │ │ │ │ │ └── page.tsx │ │ │ │ │ │ │ └── layout.tsx │ │ │ │ │ │ ├── layout.tsx │ │ │ │ │ │ ├── loading.tsx │ │ │ │ │ │ └── set-workspace-cookie.tsx │ │ │ │ │ └── layout.tsx │ │ │ │ ├── (share)/ │ │ │ │ │ ├── layout.tsx │ │ │ │ │ └── share/ │ │ │ │ │ └── [token]/ │ │ │ │ │ ├── page-client.tsx │ │ │ │ │ └── page.tsx │ │ │ │ ├── api/ │ │ │ │ │ ├── accounts/ │ │ │ │ │ │ ├── [id]/ │ │ │ │ │ │ │ └── route.ts │ │ │ │ │ │ └── route.ts │ │ │ │ │ ├── ai/ │ │ │ │ │ │ └── suggestions/ │ │ │ │ │ │ ├── prompt.ts │ │ │ │ │ │ └── route.tsx │ │ │ │ │ ├── auth/ │ │ │ │ │ │ └── [...all]/ │ │ │ │ │ │ └── route.ts │ │ │ │ │ ├── authors/ │ │ │ │ │ │ ├── [id]/ │ │ │ │ │ │ │ └── route.ts │ │ │ │ │ │ └── route.ts │ │ │ │ │ ├── categories/ │ │ │ │ │ │ ├── [id]/ │ │ │ │ │ │ │ └── route.ts │ │ │ │ │ │ └── route.ts │ │ │ │ │ ├── fields/ │ │ │ │ │ │ ├── [id]/ │ │ │ │ │ │ │ └── route.ts │ │ │ │ │ │ └── route.ts │ │ │ │ │ ├── keys/ │ │ │ │ │ │ ├── [id]/ │ │ │ │ │ │ │ └── route.ts │ │ │ │ │ │ └── route.ts │ │ │ │ │ ├── media/ │ │ │ │ │ │ ├── [id]/ │ │ │ │ │ │ │ └── route.ts │ │ │ │ │ │ ├── editor/ │ │ │ │ │ │ │ └── route.ts │ │ │ │ │ │ └── route.ts │ │ │ │ │ ├── metrics/ │ │ │ │ │ │ ├── publishing/ │ │ │ │ │ │ │ └── route.ts │ │ │ │ │ │ └── usage/ │ │ │ │ │ │ └── route.ts │ │ │ │ │ ├── polar/ │ │ │ │ │ │ └── success/ │ │ │ │ │ │ └── route.ts │ │ │ │ │ ├── posts/ │ │ │ │ │ │ ├── [id]/ │ │ │ │ │ │ │ ├── fields/ │ │ │ │ │ │ │ │ └── route.ts │ │ │ │ │ │ │ └── route.ts │ │ │ │ │ │ ├── import/ │ │ │ │ │ │ │ └── route.ts │ │ │ │ │ │ └── route.ts │ │ │ │ │ ├── share/ │ │ │ │ │ │ ├── [token]/ │ │ │ │ │ │ │ └── route.ts │ │ │ │ │ │ └── route.ts │ │ │ │ │ ├── tags/ │ │ │ │ │ │ ├── [id]/ │ │ │ │ │ │ │ └── route.ts │ │ │ │ │ │ └── route.ts │ │ │ │ │ ├── upload/ │ │ │ │ │ │ ├── complete/ │ │ │ │ │ │ │ └── route.ts │ │ │ │ │ │ └── route.ts │ │ │ │ │ ├── user/ │ │ │ │ │ │ ├── notifications/ │ │ │ │ │ │ │ └── route.ts │ │ │ │ │ │ └── route.ts │ │ │ │ │ ├── webhooks/ │ │ │ │ │ │ ├── [id]/ │ │ │ │ │ │ │ ├── route.ts │ │ │ │ │ │ │ └── test/ │ │ │ │ │ │ │ └── route.ts │ │ │ │ │ │ └── route.ts │ │ │ │ │ └── workspaces/ │ │ │ │ │ ├── [slug]/ │ │ │ │ │ │ └── route.ts │ │ │ │ │ └── route.ts │ │ │ │ ├── layout.tsx │ │ │ │ ├── not-found.tsx │ │ │ │ ├── providers.tsx │ │ │ │ └── robots.ts │ │ │ ├── components/ │ │ │ │ ├── auth/ │ │ │ │ │ ├── login-form.tsx │ │ │ │ │ ├── register-form.tsx │ │ │ │ │ ├── reset/ │ │ │ │ │ │ ├── reset-form.tsx │ │ │ │ │ │ └── reset-request-form.tsx │ │ │ │ │ └── verify-form.tsx │ │ │ │ ├── authors/ │ │ │ │ │ ├── author-modals.tsx │ │ │ │ │ ├── author-sheet.tsx │ │ │ │ │ ├── columns.tsx │ │ │ │ │ ├── data-table.tsx │ │ │ │ │ └── table-actions.tsx │ │ │ │ ├── billing/ │ │ │ │ │ ├── success-modal.tsx │ │ │ │ │ └── upgrade-modal.tsx │ │ │ │ ├── categories/ │ │ │ │ │ ├── category-modals.tsx │ │ │ │ │ ├── columns.tsx │ │ │ │ │ ├── data-table.tsx │ │ │ │ │ └── table-actions.tsx │ │ │ │ ├── editor/ │ │ │ │ │ ├── ai/ │ │ │ │ │ │ └── readability-suggestions.tsx │ │ │ │ │ ├── editor-data-provider.tsx │ │ │ │ │ ├── editor-header.tsx │ │ │ │ │ ├── editor-page.tsx │ │ │ │ │ ├── editor-sidebar.tsx │ │ │ │ │ ├── editor.tsx │ │ │ │ │ ├── fields/ │ │ │ │ │ │ ├── author-selector.tsx │ │ │ │ │ │ ├── category-selector.tsx │ │ │ │ │ │ ├── cover-image-selector.tsx │ │ │ │ │ │ ├── custom-fields-section.tsx │ │ │ │ │ │ ├── description-field.tsx │ │ │ │ │ │ ├── featured-field.tsx │ │ │ │ │ │ ├── field-info.tsx │ │ │ │ │ │ ├── publish-date-field.tsx │ │ │ │ │ │ ├── slug-field.tsx │ │ │ │ │ │ ├── status-field.tsx │ │ │ │ │ │ └── tag-selector.tsx │ │ │ │ │ ├── footer/ │ │ │ │ │ │ └── metadata-footer.tsx │ │ │ │ │ ├── link-selector.tsx │ │ │ │ │ ├── share-modal.tsx │ │ │ │ │ ├── tabs/ │ │ │ │ │ │ ├── analysis-tab.tsx │ │ │ │ │ │ └── metadata-tab.tsx │ │ │ │ │ └── textarea-autosize.tsx │ │ │ │ ├── fields/ │ │ │ │ │ ├── create-custom-field.tsx │ │ │ │ │ ├── custom-field-row.tsx │ │ │ │ │ ├── delete-custom-field.tsx │ │ │ │ │ ├── edit-custom-field.tsx │ │ │ │ │ └── field-options-input.tsx │ │ │ │ ├── home/ │ │ │ │ │ ├── api-usage-card.tsx │ │ │ │ │ ├── media-usage-card.tsx │ │ │ │ │ ├── publishing-activity-card.tsx │ │ │ │ │ └── webhook-usage-card.tsx │ │ │ │ ├── icons/ │ │ │ │ │ ├── marble.tsx │ │ │ │ │ └── social/ │ │ │ │ │ └── index.tsx │ │ │ │ ├── invoice/ │ │ │ │ │ ├── columns.tsx │ │ │ │ │ ├── data-table.tsx │ │ │ │ │ └── table-actions.tsx │ │ │ │ ├── keys/ │ │ │ │ │ ├── api-key-modal.tsx │ │ │ │ │ ├── columns.tsx │ │ │ │ │ ├── data-table.tsx │ │ │ │ │ ├── delete-key.tsx │ │ │ │ │ └── table-actions.tsx │ │ │ │ ├── layout/ │ │ │ │ │ ├── header-sidebar-trigger.tsx │ │ │ │ │ ├── page-header.tsx │ │ │ │ │ └── wrapper.tsx │ │ │ │ ├── media/ │ │ │ │ │ ├── crop-image-modal.tsx │ │ │ │ │ ├── delete-modal.tsx │ │ │ │ │ ├── file-upload-input.tsx │ │ │ │ │ ├── media-actions.tsx │ │ │ │ │ ├── media-card.tsx │ │ │ │ │ ├── media-columns.tsx │ │ │ │ │ ├── media-controls.tsx │ │ │ │ │ ├── media-data-table.tsx │ │ │ │ │ ├── media-gallery.tsx │ │ │ │ │ ├── media-table-toolbar.tsx │ │ │ │ │ ├── upload-modal.tsx │ │ │ │ │ └── video-player.tsx │ │ │ │ ├── nav/ │ │ │ │ │ ├── announcements.tsx │ │ │ │ │ ├── app-breadcrumb.tsx │ │ │ │ │ ├── app-sidebar.tsx │ │ │ │ │ ├── create-workspace-dialog.tsx │ │ │ │ │ ├── nav-extra.tsx │ │ │ │ │ ├── nav-main.tsx │ │ │ │ │ ├── nav-settings.tsx │ │ │ │ │ ├── nav-user.tsx │ │ │ │ │ ├── sidebar-footer-content.tsx │ │ │ │ │ ├── theme-toggle.tsx │ │ │ │ │ ├── upgrade-card.tsx │ │ │ │ │ ├── whats-new-card.tsx │ │ │ │ │ └── workspace-switcher.tsx │ │ │ │ ├── posts/ │ │ │ │ │ ├── columns.tsx │ │ │ │ │ ├── data-grid.tsx │ │ │ │ │ ├── data-table.tsx │ │ │ │ │ ├── data-view.tsx │ │ │ │ │ ├── import-item-form.tsx │ │ │ │ │ ├── import-modal.tsx │ │ │ │ │ ├── post-actions.tsx │ │ │ │ │ └── post-modals.tsx │ │ │ │ ├── settings/ │ │ │ │ │ ├── account.tsx │ │ │ │ │ ├── delete-account.tsx │ │ │ │ │ ├── fields/ │ │ │ │ │ │ ├── delete.tsx │ │ │ │ │ │ ├── id.tsx │ │ │ │ │ │ ├── logo.tsx │ │ │ │ │ │ ├── name.tsx │ │ │ │ │ │ ├── slug.tsx │ │ │ │ │ │ └── timezone.tsx │ │ │ │ │ ├── section.tsx │ │ │ │ │ └── theme.tsx │ │ │ │ ├── share/ │ │ │ │ │ ├── prose.tsx │ │ │ │ │ └── screens.tsx │ │ │ │ ├── shared/ │ │ │ │ │ ├── container.tsx │ │ │ │ │ ├── dropzone.tsx │ │ │ │ │ ├── icons.tsx │ │ │ │ │ ├── page-loader.tsx │ │ │ │ │ └── pending-state.tsx │ │ │ │ ├── tags/ │ │ │ │ │ ├── columns.tsx │ │ │ │ │ ├── data-table.tsx │ │ │ │ │ ├── table-actions.tsx │ │ │ │ │ └── tag-modals.tsx │ │ │ │ ├── team/ │ │ │ │ │ ├── columns.tsx │ │ │ │ │ ├── data-table.tsx │ │ │ │ │ ├── invite-button.tsx │ │ │ │ │ ├── invite-modal.tsx │ │ │ │ │ ├── invite-section.tsx │ │ │ │ │ ├── leave-workspace.tsx │ │ │ │ │ ├── profile-sheet.tsx │ │ │ │ │ ├── table-actions.tsx │ │ │ │ │ └── team-modals.tsx │ │ │ │ ├── ui/ │ │ │ │ │ ├── async-button.tsx │ │ │ │ │ ├── copy-button.tsx │ │ │ │ │ ├── data-table-pagination.tsx │ │ │ │ │ ├── error-message.tsx │ │ │ │ │ ├── gauge.tsx │ │ │ │ │ ├── hidden-scrollbar.tsx │ │ │ │ │ ├── last-used-badge.tsx │ │ │ │ │ ├── loading-spinner.tsx │ │ │ │ │ ├── segmented-progress.tsx │ │ │ │ │ └── timezone-selector.tsx │ │ │ │ └── webhooks/ │ │ │ │ ├── create-webhook.tsx │ │ │ │ ├── delete-webhook.tsx │ │ │ │ ├── edit-webhook.tsx │ │ │ │ ├── webhook-actions.tsx │ │ │ │ ├── webhook-card.tsx │ │ │ │ ├── webhook-columns.tsx │ │ │ │ └── webhook-data-table.tsx │ │ │ ├── hooks/ │ │ │ │ ├── use-debounce.ts │ │ │ │ ├── use-isomorphic-layout-effect.ts │ │ │ │ ├── use-localstorage.ts │ │ │ │ ├── use-media-actions.ts │ │ │ │ ├── use-media-query.ts │ │ │ │ ├── use-mobile.tsx │ │ │ │ ├── use-plan.ts │ │ │ │ ├── use-readability.ts │ │ │ │ └── use-workspace-id.ts │ │ │ ├── lib/ │ │ │ │ ├── actions/ │ │ │ │ │ ├── checks.ts │ │ │ │ │ ├── email.ts │ │ │ │ │ ├── user.ts │ │ │ │ │ └── workspace.ts │ │ │ │ ├── ai/ │ │ │ │ │ └── readability.ts │ │ │ │ ├── auth/ │ │ │ │ │ ├── access.ts │ │ │ │ │ ├── client.ts │ │ │ │ │ ├── redirect.ts │ │ │ │ │ ├── server.ts │ │ │ │ │ ├── session.ts │ │ │ │ │ ├── types.ts │ │ │ │ │ └── workspace.ts │ │ │ │ ├── blurhash.ts │ │ │ │ ├── cache/ │ │ │ │ │ └── invalidate.ts │ │ │ │ ├── cache.ts │ │ │ │ ├── constants.ts │ │ │ │ ├── custom-fields.ts │ │ │ │ ├── data/ │ │ │ │ │ └── post.ts │ │ │ │ ├── events/ │ │ │ │ │ └── dispatch.ts │ │ │ │ ├── media/ │ │ │ │ │ └── upload.ts │ │ │ │ ├── notifications.ts │ │ │ │ ├── plans.ts │ │ │ │ ├── polar/ │ │ │ │ │ ├── client.ts │ │ │ │ │ ├── customer.created.ts │ │ │ │ │ ├── subscription.canceled.ts │ │ │ │ │ ├── subscription.created.ts │ │ │ │ │ ├── subscription.revoked.ts │ │ │ │ │ ├── subscription.updated.ts │ │ │ │ │ └── utils.ts │ │ │ │ ├── queries/ │ │ │ │ │ ├── keys.ts │ │ │ │ │ ├── user.ts │ │ │ │ │ └── workspace.ts │ │ │ │ ├── r2.ts │ │ │ │ ├── ratelimit.ts │ │ │ │ ├── redis.ts │ │ │ │ ├── search-params.ts │ │ │ │ └── validations/ │ │ │ │ ├── auth.ts │ │ │ │ ├── authors.ts │ │ │ │ ├── editor.ts │ │ │ │ ├── fields.ts │ │ │ │ ├── keys.ts │ │ │ │ ├── post.ts │ │ │ │ ├── settings.ts │ │ │ │ ├── tags.ts │ │ │ │ ├── upload.ts │ │ │ │ ├── webhook.ts │ │ │ │ └── workspace.ts │ │ │ ├── providers/ │ │ │ │ ├── user.tsx │ │ │ │ └── workspace.tsx │ │ │ ├── proxy.ts │ │ │ ├── styles/ │ │ │ │ ├── editor.css │ │ │ │ └── globals.css │ │ │ ├── types/ │ │ │ │ ├── author.ts │ │ │ │ ├── dashboard.ts │ │ │ │ ├── fields.ts │ │ │ │ ├── icons.ts │ │ │ │ ├── media.ts │ │ │ │ ├── misc.ts │ │ │ │ ├── share.ts │ │ │ │ ├── user.ts │ │ │ │ ├── webhook.ts │ │ │ │ └── workspace.ts │ │ │ └── utils/ │ │ │ ├── author.tsx │ │ │ ├── editor.ts │ │ │ ├── fetch/ │ │ │ │ └── client.ts │ │ │ ├── keys.ts │ │ │ ├── media.ts │ │ │ ├── readability.ts │ │ │ ├── site.ts │ │ │ ├── string.ts │ │ │ ├── usage/ │ │ │ │ └── media.ts │ │ │ └── workspace/ │ │ │ ├── client.ts │ │ │ ├── constants.ts │ │ │ └── server.ts │ │ └── tsconfig.json │ ├── jobs/ │ │ ├── .gitignore │ │ ├── README.md │ │ ├── package.json │ │ ├── src/ │ │ │ ├── consumers/ │ │ │ │ ├── deliveries.ts │ │ │ │ ├── dlq.ts │ │ │ │ └── events.ts │ │ │ ├── index.ts │ │ │ ├── lib/ │ │ │ │ ├── db.ts │ │ │ │ ├── formats.ts │ │ │ │ ├── signing.ts │ │ │ │ └── usage.ts │ │ │ ├── scheduled/ │ │ │ │ └── cleanup.ts │ │ │ └── types/ │ │ │ └── env.ts │ │ ├── tsconfig.json │ │ └── wrangler.jsonc │ ├── mcp/ │ │ ├── .gitignore │ │ ├── README.md │ │ ├── package.json │ │ ├── public/ │ │ │ ├── home.js │ │ │ └── styles.css │ │ ├── src/ │ │ │ ├── components/ │ │ │ │ ├── icons.tsx │ │ │ │ └── mcp-clients.tsx │ │ │ ├── index.ts │ │ │ ├── lib/ │ │ │ │ ├── api.ts │ │ │ │ ├── auth.ts │ │ │ │ ├── constants.ts │ │ │ │ ├── instructions.ts │ │ │ │ ├── mcp.ts │ │ │ │ └── media.ts │ │ │ ├── routes/ │ │ │ │ ├── home.tsx │ │ │ │ └── mcp.ts │ │ │ ├── server.ts │ │ │ ├── tools/ │ │ │ │ ├── authors.ts │ │ │ │ ├── categories.ts │ │ │ │ ├── media.ts │ │ │ │ ├── posts.ts │ │ │ │ ├── shared.ts │ │ │ │ └── tags.ts │ │ │ └── types.ts │ │ ├── tsconfig.json │ │ └── wrangler.jsonc │ └── web/ │ ├── .gitignore │ ├── .vscode/ │ │ └── launch.json │ ├── README.md │ ├── astro.config.mjs │ ├── package.json │ ├── public/ │ │ ├── robots.txt │ │ └── site.webmanifest │ ├── src/ │ │ ├── components/ │ │ │ ├── BlogHeader.astro │ │ │ ├── CategoryCard.astro │ │ │ ├── CategoryFilter.astro │ │ │ ├── ChangelogCard.astro │ │ │ ├── Container.astro │ │ │ ├── Footer.astro │ │ │ ├── Head.astro │ │ │ ├── Header.astro │ │ │ ├── PostCard.astro │ │ │ ├── PricingCard.astro │ │ │ ├── Prose.astro │ │ │ ├── ScrollToTop.astro │ │ │ ├── Welcome.astro │ │ │ ├── icons/ │ │ │ │ ├── Collab.astro │ │ │ │ ├── Discord.astro │ │ │ │ ├── Github.astro │ │ │ │ ├── Logo.astro │ │ │ │ ├── Media.astro │ │ │ │ ├── WordMark.astro │ │ │ │ ├── WordMarkAlt.astro │ │ │ │ ├── X.astro │ │ │ │ ├── brand/ │ │ │ │ │ ├── Bounty.astro │ │ │ │ │ ├── Candle.astro │ │ │ │ │ ├── Databuddy.astro │ │ │ │ │ ├── Helix.astro │ │ │ │ │ ├── Ia.astro │ │ │ │ │ ├── Mantlz.astro │ │ │ │ │ └── Opencut.astro │ │ │ │ └── sponsors/ │ │ │ │ ├── Neon.astro │ │ │ │ ├── Upstash.astro │ │ │ │ └── Vercel.astro │ │ │ ├── sections/ │ │ │ │ └── Pricing.astro │ │ │ └── ui/ │ │ │ ├── AccordionItem.astro │ │ │ └── Button.astro │ │ ├── content/ │ │ │ └── pages/ │ │ │ ├── privacy.md │ │ │ └── terms.md │ │ ├── content.config.ts │ │ ├── layouts/ │ │ │ ├── BlogLayout.astro │ │ │ └── Layout.astro │ │ ├── lib/ │ │ │ ├── accordion.ts │ │ │ ├── constants/ │ │ │ │ ├── faqs.ts │ │ │ │ ├── landing.ts │ │ │ │ ├── navigation.ts │ │ │ │ ├── site.ts │ │ │ │ └── tracking.ts │ │ │ ├── marble.ts │ │ │ ├── schemas.ts │ │ │ ├── seo.ts │ │ │ ├── site.ts │ │ │ └── utils.ts │ │ ├── pages/ │ │ │ ├── 404.astro │ │ │ ├── blog/ │ │ │ │ ├── [slug].astro │ │ │ │ ├── category/ │ │ │ │ │ └── [slug].astro │ │ │ │ └── index.astro │ │ │ ├── changelog/ │ │ │ │ ├── [slug].astro │ │ │ │ └── index.astro │ │ │ ├── contributors/ │ │ │ │ └── index.astro │ │ │ ├── index.astro │ │ │ ├── pricing/ │ │ │ │ └── index.astro │ │ │ ├── privacy/ │ │ │ │ └── index.astro │ │ │ ├── rss.xml.ts │ │ │ ├── sponsors/ │ │ │ │ └── index.astro │ │ │ └── terms/ │ │ │ └── index.astro │ │ └── styles/ │ │ └── globals.css │ ├── tailwind.config.ts │ └── tsconfig.json ├── biome.jsonc ├── commitlint.config.ts ├── docker-compose.yml ├── package.json ├── packages/ │ ├── db/ │ │ ├── .gitignore │ │ ├── package.json │ │ ├── prisma/ │ │ │ ├── migrations/ │ │ │ │ ├── 0_init/ │ │ │ │ │ └── migration.sql │ │ │ │ ├── 20250831193214_add_author_table/ │ │ │ │ │ └── migration.sql │ │ │ │ ├── 20250907120320_make_new_primary_author_required/ │ │ │ │ │ └── migration.sql │ │ │ │ ├── 20250907125704_drop_legacy_user_author_fields/ │ │ │ │ │ └── migration.sql │ │ │ │ ├── 20250907194746_rename_author_fields_to_final_names/ │ │ │ │ │ └── migration.sql │ │ │ │ ├── 20250908090455_make_primary_author_optional/ │ │ │ │ │ └── migration.sql │ │ │ │ ├── 20250909162749_make_published_at_optional/ │ │ │ │ │ └── migration.sql │ │ │ │ ├── 20250909171017_make_published_at_required/ │ │ │ │ │ └── migration.sql │ │ │ │ ├── 20250911083948_add_slack_payload_format/ │ │ │ │ │ └── migration.sql │ │ │ │ ├── 20250915114755_add_database_indices/ │ │ │ │ │ └── migration.sql │ │ │ │ ├── 20250919210238_add_share_link_table/ │ │ │ │ │ └── migration.sql │ │ │ │ ├── 20250923212858_add_ai_editor_preferences/ │ │ │ │ │ └── migration.sql │ │ │ │ ├── 20250924180405_add_missing_better_auth_indices/ │ │ │ │ │ └── migration.sql │ │ │ │ ├── 20250927161627_add_author_social_links/ │ │ │ │ │ └── migration.sql │ │ │ │ ├── 20251114225009_add_usage_event_table/ │ │ │ │ │ └── migration.sql │ │ │ │ ├── 20251116173412_new_media_enum_and_alt_text_column/ │ │ │ │ │ └── migration.sql │ │ │ │ ├── 20251201001521_add_api_keys/ │ │ │ │ │ └── migration.sql │ │ │ │ ├── 20251210213108_subscription_history/ │ │ │ │ │ └── migration.sql │ │ │ │ ├── 20260331143009_add_fields/ │ │ │ │ │ └── migration.sql │ │ │ │ ├── 20260505135201_add_media_metadata/ │ │ │ │ │ └── migration.sql │ │ │ │ ├── 20260508223056_add_notification_preferences/ │ │ │ │ │ └── migration.sql │ │ │ │ ├── 20260511_rename_webhook_to_webhook_endpoint/ │ │ │ │ │ └── migration.sql │ │ │ │ ├── 20260513192507_add_workspace_events/ │ │ │ │ │ └── migration.sql │ │ │ │ ├── 20260515000100_add_subscription_polar_event_ordering/ │ │ │ │ │ └── migration.sql │ │ │ │ └── migration_lock.toml │ │ │ └── schema.prisma │ │ ├── prisma.config.ts │ │ ├── src/ │ │ │ ├── browser.ts │ │ │ ├── client.ts │ │ │ ├── hyperdrive.ts │ │ │ ├── index.ts │ │ │ └── workers.ts │ │ └── tsconfig.json │ ├── demo-markdown.md │ ├── editor/ │ │ ├── package.json │ │ ├── src/ │ │ │ ├── components/ │ │ │ │ ├── color-picker.tsx │ │ │ │ ├── editor-character-count.tsx │ │ │ │ ├── editor-content.tsx │ │ │ │ ├── editor-provider.tsx │ │ │ │ ├── editor-table-menus.tsx │ │ │ │ ├── icons/ │ │ │ │ │ ├── twitter.tsx │ │ │ │ │ └── youtube.tsx │ │ │ │ ├── index.ts │ │ │ │ ├── marks/ │ │ │ │ │ ├── editor-clear-formatting.tsx │ │ │ │ │ ├── editor-link-selector.tsx │ │ │ │ │ ├── editor-mark-bold.tsx │ │ │ │ │ ├── editor-mark-code.tsx │ │ │ │ │ ├── editor-mark-highlight.tsx │ │ │ │ │ ├── editor-mark-italic.tsx │ │ │ │ │ ├── editor-mark-strike.tsx │ │ │ │ │ ├── editor-mark-subscript.tsx │ │ │ │ │ ├── editor-mark-superscript.tsx │ │ │ │ │ ├── editor-mark-text-color.tsx │ │ │ │ │ ├── editor-mark-underline.tsx │ │ │ │ │ └── index.ts │ │ │ │ ├── menus/ │ │ │ │ │ ├── block-handle-menu.tsx │ │ │ │ │ ├── bubble-menu.tsx │ │ │ │ │ ├── floating-menu.tsx │ │ │ │ │ └── index.ts │ │ │ │ ├── nodes/ │ │ │ │ │ ├── editor-align-selector.tsx │ │ │ │ │ ├── editor-align.tsx │ │ │ │ │ ├── editor-node-bullet-list.tsx │ │ │ │ │ ├── editor-node-code.tsx │ │ │ │ │ ├── editor-node-heading1.tsx │ │ │ │ │ ├── editor-node-heading2.tsx │ │ │ │ │ ├── editor-node-heading3.tsx │ │ │ │ │ ├── editor-node-ordered-list.tsx │ │ │ │ │ ├── editor-node-quote.tsx │ │ │ │ │ ├── editor-node-table.tsx │ │ │ │ │ ├── editor-node-task-list.tsx │ │ │ │ │ ├── editor-node-text.tsx │ │ │ │ │ └── index.ts │ │ │ │ ├── rich-text-field.tsx │ │ │ │ └── ui/ │ │ │ │ ├── editor-button.tsx │ │ │ │ ├── editor-selector.tsx │ │ │ │ └── index.ts │ │ │ ├── extensions/ │ │ │ │ ├── code-block/ │ │ │ │ │ ├── code-block-comp.tsx │ │ │ │ │ ├── code-block-view.tsx │ │ │ │ │ ├── code-block.ts │ │ │ │ │ └── index.ts │ │ │ │ ├── extension-kit.ts │ │ │ │ ├── figure/ │ │ │ │ │ ├── figure-view.tsx │ │ │ │ │ └── index.ts │ │ │ │ ├── image-upload/ │ │ │ │ │ ├── hooks.ts │ │ │ │ │ ├── image-upload-comp.tsx │ │ │ │ │ ├── image-upload-view.tsx │ │ │ │ │ └── index.ts │ │ │ │ ├── index.ts │ │ │ │ ├── markdown-input/ │ │ │ │ │ ├── index.ts │ │ │ │ │ └── utils.ts │ │ │ │ ├── slash-command/ │ │ │ │ │ ├── groups.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── menu-list.tsx │ │ │ │ │ └── slash-command.ts │ │ │ │ ├── table/ │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── menus/ │ │ │ │ │ │ ├── table-column/ │ │ │ │ │ │ │ ├── index.tsx │ │ │ │ │ │ │ └── utils.ts │ │ │ │ │ │ └── table-row/ │ │ │ │ │ │ ├── index.tsx │ │ │ │ │ │ └── utils.ts │ │ │ │ │ ├── table-cell.ts │ │ │ │ │ ├── table-header.ts │ │ │ │ │ ├── table-row.ts │ │ │ │ │ ├── table.ts │ │ │ │ │ └── utils.ts │ │ │ │ ├── twitter/ │ │ │ │ │ ├── index.tsx │ │ │ │ │ ├── twitter-comp.tsx │ │ │ │ │ ├── twitter-upload.ts │ │ │ │ │ └── twitter-view.tsx │ │ │ │ ├── video/ │ │ │ │ │ ├── index.ts │ │ │ │ │ └── video-view.tsx │ │ │ │ ├── video-upload/ │ │ │ │ │ ├── hooks.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── video-upload-comp.tsx │ │ │ │ │ └── video-upload-view.tsx │ │ │ │ └── youtube/ │ │ │ │ ├── youtube-comp.tsx │ │ │ │ ├── youtube-upload.ts │ │ │ │ └── youtube-view.tsx │ │ │ ├── index.ts │ │ │ ├── lib/ │ │ │ │ ├── index.ts │ │ │ │ ├── lowlight.ts │ │ │ │ └── utils.ts │ │ │ ├── styles/ │ │ │ │ ├── color-picker.css │ │ │ │ ├── table.css │ │ │ │ └── task-list.css │ │ │ └── types/ │ │ │ └── index.ts │ │ └── tsconfig.json │ ├── email/ │ │ ├── package.json │ │ ├── src/ │ │ │ ├── components/ │ │ │ │ ├── button.tsx │ │ │ │ └── footer.tsx │ │ │ ├── emails/ │ │ │ │ ├── founder.tsx │ │ │ │ ├── invite.tsx │ │ │ │ ├── reset.tsx │ │ │ │ ├── usage-limit.tsx │ │ │ │ ├── verify.tsx │ │ │ │ └── welcome.tsx │ │ │ ├── index.ts │ │ │ └── lib/ │ │ │ ├── config.ts │ │ │ ├── dev.ts │ │ │ └── send.ts │ │ └── tsconfig.json │ ├── events/ │ │ ├── package.json │ │ ├── src/ │ │ │ ├── index.ts │ │ │ ├── types.ts │ │ │ └── utils/ │ │ │ ├── demo.ts │ │ │ ├── envelope.ts │ │ │ ├── events.ts │ │ │ └── resources.ts │ │ └── tsconfig.json │ ├── parser/ │ │ ├── .gitignore │ │ ├── package.json │ │ ├── src/ │ │ │ └── tiptap.ts │ │ ├── tests/ │ │ │ └── tiptap-parser.test.ts │ │ └── tsconfig.json │ ├── tsconfig/ │ │ ├── base.json │ │ ├── nextjs.json │ │ ├── package.json │ │ └── react-library.json │ ├── ui/ │ │ ├── README.md │ │ ├── components.json │ │ ├── package.json │ │ ├── postcss.config.mjs │ │ ├── src/ │ │ │ ├── components/ │ │ │ │ ├── alert-dialog.tsx │ │ │ │ ├── avatar.tsx │ │ │ │ ├── badge.tsx │ │ │ │ ├── breadcrumb.tsx │ │ │ │ ├── button.tsx │ │ │ │ ├── calendar.tsx │ │ │ │ ├── card.tsx │ │ │ │ ├── chart.tsx │ │ │ │ ├── checkbox.tsx │ │ │ │ ├── collapsible.tsx │ │ │ │ ├── command.tsx │ │ │ │ ├── dialog.tsx │ │ │ │ ├── drawer.tsx │ │ │ │ ├── dropdown-menu.tsx │ │ │ │ ├── input-otp.tsx │ │ │ │ ├── input.tsx │ │ │ │ ├── kibo-ui/ │ │ │ │ │ ├── contribution-graph/ │ │ │ │ │ │ └── index.tsx │ │ │ │ │ └── image-crop/ │ │ │ │ │ └── index.tsx │ │ │ │ ├── label.tsx │ │ │ │ ├── pagination.tsx │ │ │ │ ├── popover.tsx │ │ │ │ ├── progress.tsx │ │ │ │ ├── radio-group.tsx │ │ │ │ ├── scroll-area.tsx │ │ │ │ ├── select.tsx │ │ │ │ ├── separator.tsx │ │ │ │ ├── sheet.tsx │ │ │ │ ├── sidebar.tsx │ │ │ │ ├── skeleton.tsx │ │ │ │ ├── sonner.tsx │ │ │ │ ├── switch.tsx │ │ │ │ ├── table.tsx │ │ │ │ ├── tabs.tsx │ │ │ │ ├── textarea.tsx │ │ │ │ ├── toggle-group.tsx │ │ │ │ ├── toggle.tsx │ │ │ │ └── tooltip.tsx │ │ │ ├── hooks/ │ │ │ │ └── use-mobile.ts │ │ │ ├── lib/ │ │ │ │ └── utils.ts │ │ │ └── styles/ │ │ │ └── globals.css │ │ └── tsconfig.json │ └── utils/ │ ├── package.json │ ├── src/ │ │ ├── constants/ │ │ │ ├── api-key.ts │ │ │ ├── plans.ts │ │ │ ├── pricing.ts │ │ │ └── site.ts │ │ ├── functions/ │ │ │ ├── api-key.ts │ │ │ ├── highlight.ts │ │ │ └── webhooks.ts │ │ ├── index.ts │ │ └── types/ │ │ └── api-key.ts │ └── tsconfig.json ├── pnpm-workspace.yaml ├── skills-lock.json ├── tsconfig.json └── turbo.json
SYMBOL INDEX (1484 symbols across 482 files)
FILE: apps/api/src/lib/cache.ts
constant DEFAULT_TTL (line 4) | const DEFAULT_TTL = 3600;
constant CACHE_PREFIX (line 7) | const CACHE_PREFIX = "cache";
constant MAX_CACHE_VALUE_BYTES (line 10) | const MAX_CACHE_VALUE_BYTES = 8 * 1024 * 1024;
type CacheClient (line 12) | type CacheClient = ReturnType<typeof createCacheClient>;
function createCacheClient (line 18) | function createCacheClient(url: string, token: string) {
function cacheKey (line 180) | function cacheKey(
function hashQueryParams (line 192) | function hashQueryParams(params: Record<string, unknown>): string {
FILE: apps/api/src/lib/constants.ts
constant ROUTES (line 5) | const ROUTES = [
constant MAX_UPLOAD_SIZE (line 14) | const MAX_UPLOAD_SIZE = 5 * 1024 * 1024;
constant DEFAULT_CDN_URL (line 15) | const DEFAULT_CDN_URL = "https://cdn.marblecms.com";
constant ALLOWED_IMAGE_MIME_TYPES (line 17) | const ALLOWED_IMAGE_MIME_TYPES = [
constant ALLOWED_VIDEO_MIME_TYPES (line 26) | const ALLOWED_VIDEO_MIME_TYPES = [
constant ALLOWED_AUDIO_MIME_TYPES (line 33) | const ALLOWED_AUDIO_MIME_TYPES = [
constant ALLOWED_DOCUMENT_MIME_TYPES (line 43) | const ALLOWED_DOCUMENT_MIME_TYPES = [
constant ALLOWED_MEDIA_MIME_TYPES (line 57) | const ALLOWED_MEDIA_MIME_TYPES = [
type AllowedMediaMimeType (line 64) | type AllowedMediaMimeType = (typeof ALLOWED_MEDIA_MIME_TYPES)[number];
constant FRAMER_PLUGIN_ID (line 66) | const FRAMER_PLUGIN_ID = "4pj5owtk2qcexo6c1yt9kicye";
constant FRAMER_PLUGIN_PATTERN (line 67) | const FRAMER_PLUGIN_PATTERN = new RegExp(
FILE: apps/api/src/lib/crypto.ts
function hashApiKey (line 12) | async function hashApiKey(key: string): Promise<string> {
FILE: apps/api/src/lib/db.ts
function getConnectionString (line 11) | function getConnectionString(env: Env): string {
type DbClient (line 28) | type DbClient = ReturnType<typeof createDbClient>;
function createDbClient (line 30) | function createDbClient(env: Env) {
FILE: apps/api/src/lib/events.ts
type EmitEventOptions (line 10) | interface EmitEventOptions {
function emitEvent (line 21) | async function emitEvent(
FILE: apps/api/src/lib/media.ts
type MediaType (line 4) | type MediaType = "image" | "video" | "audio" | "document";
type MediaRecord (line 6) | interface MediaRecord {
function serializeMedia (line 22) | function serializeMedia(item: MediaRecord) {
function getMediaType (line 40) | function getMediaType(mimeType: string): MediaType {
function extensionFromFile (line 53) | function extensionFromFile(file: File) {
function publicUrl (line 64) | function publicUrl(envUrl: string | undefined, key: string) {
function objectKeyFromUrl (line 69) | function objectKeyFromUrl(url: string) {
function getImageDimensions (line 77) | function getImageDimensions(buffer: ArrayBuffer) {
FILE: apps/api/src/lib/polar.ts
function createPolarClient (line 3) | function createPolarClient(accessToken: string, isProduction = false) {
FILE: apps/api/src/lib/posts.ts
function castFieldValue (line 8) | function castFieldValue(
function buildFieldsObject (line 30) | function buildFieldsObject(
FILE: apps/api/src/lib/redis.ts
function createRedisClient (line 3) | function createRedisClient(url: string, token: string): Redis {
FILE: apps/api/src/lib/usage.ts
type DbClient (line 7) | type DbClient = ReturnType<typeof createDbClient>;
constant USAGE_KEY_PREFIX (line 9) | const USAGE_KEY_PREFIX = "usage:api";
constant USAGE_META_PREFIX (line 10) | const USAGE_META_PREFIX = "usage:meta";
constant META_TTL (line 12) | const META_TTL = 300;
type UsageMeta (line 14) | interface UsageMeta {
type BillingPeriod (line 20) | interface BillingPeriod {
function getBillingPeriod (line 25) | async function getBillingPeriod(
type UsageCheckResult (line 102) | interface UsageCheckResult {
function getUsageMeta (line 111) | async function getUsageMeta(
function seedUsageCounter (line 154) | async function seedUsageCounter(
function checkApiUsage (line 179) | async function checkApiUsage(
function checkApiUsageFromDb (line 240) | async function checkApiUsageFromDb(
function notifyApiUsageThreshold (line 297) | async function notifyApiUsageThreshold(
FILE: apps/api/src/middleware/analytics.ts
type AnalyticsTaskParams (line 11) | interface AnalyticsTaskParams {
function runAnalyticsTask (line 24) | async function runAnalyticsTask({
function checkUsage (line 111) | async function checkUsage(
FILE: apps/api/src/middleware/cache.ts
constant DEFAULT_STALE_IF_ERROR (line 7) | const DEFAULT_STALE_IF_ERROR = 3600;
type CacheOptions (line 9) | interface CacheOptions {
FILE: apps/api/src/middleware/ratelimit.ts
type RateLimit (line 5) | interface RateLimit {
type RateLimitMode (line 14) | type RateLimitMode = "workspace" | "apiKey";
FILE: apps/api/src/routes/media.ts
function isUploadedFile (line 36) | function isUploadedFile(value: unknown): value is File {
FILE: apps/api/src/types/env.ts
type Env (line 1) | interface Env {
type ApiKeyVariables (line 16) | interface ApiKeyVariables {
type ApiKeyApp (line 23) | interface ApiKeyApp {
FILE: apps/api/src/validations/json.ts
type JsonValue (line 10) | type JsonValue =
type JsonObject (line 15) | interface JsonObject {
FILE: apps/api/src/validations/misc.ts
constant WORKSPACE_EVENT_TYPES (line 10) | const WORKSPACE_EVENT_TYPES = EVENT_TYPES;
constant WORKSPACE_EVENT_SOURCES (line 11) | const WORKSPACE_EVENT_SOURCES = EVENT_SOURCES;
constant WORKSPACE_EVENT_ACTOR_TYPES (line 12) | const WORKSPACE_EVENT_ACTOR_TYPES = EVENT_ACTOR_TYPES;
constant WORKSPACE_EVENT_RESOURCE_TYPES (line 13) | const WORKSPACE_EVENT_RESOURCE_TYPES = EVENT_RESOURCE_TYPES;
FILE: apps/cms/next.config.ts
method redirects (line 19) | async redirects() {
FILE: apps/cms/src/app/(auth)/join/[id]/page-client.tsx
type PageClientProps (line 32) | interface PageClientProps {
type GetOrganizationResponse (line 45) | interface GetOrganizationResponse {
type InviteStatus (line 58) | type InviteStatus = "pending" | "accepted" | "rejected";
function PageClient (line 60) | function PageClient({ id, user }: PageClientProps) {
function InviteError (line 261) | function InviteError() {
function InviteLoading (line 293) | function InviteLoading() {
FILE: apps/cms/src/app/(auth)/join/[id]/page.tsx
function InvitePage (line 7) | async function InvitePage(props: {
function InvitePageComponent (line 22) | async function InvitePageComponent({ code }: { code: string }) {
FILE: apps/cms/src/app/(auth)/layout.tsx
function AuthLayout (line 1) | function AuthLayout({
FILE: apps/cms/src/app/(auth)/login/page.tsx
type PageProps (line 18) | interface PageProps {
function LoginPage (line 23) | async function LoginPage(props: PageProps) {
FILE: apps/cms/src/app/(auth)/new/page-client.tsx
function PageClientInner (line 31) | function PageClientInner() {
function PageClient (line 194) | function PageClient() {
FILE: apps/cms/src/app/(auth)/new/page.tsx
function Page (line 9) | async function Page() {
FILE: apps/cms/src/app/(auth)/register/page.tsx
type PageProps (line 17) | interface PageProps {
function RegisterPage (line 22) | async function RegisterPage(props: PageProps) {
FILE: apps/cms/src/app/(auth)/reset/page.tsx
type PageProps (line 11) | interface PageProps {
function ResetPage (line 15) | async function ResetPage({ searchParams }: PageProps) {
FILE: apps/cms/src/app/(auth)/verify/page.tsx
type PageProps (line 10) | interface PageProps {
function VerifyPage (line 15) | async function VerifyPage({ searchParams }: PageProps) {
FILE: apps/cms/src/app/(main)/[workspace]/(dashboard)/(home)/page-client.tsx
function PageClient (line 15) | function PageClient() {
FILE: apps/cms/src/app/(main)/[workspace]/(dashboard)/(home)/page.tsx
function Page (line 8) | async function Page() {
FILE: apps/cms/src/app/(main)/[workspace]/(dashboard)/authors/page-client.tsx
function PageClient (line 14) | function PageClient() {
FILE: apps/cms/src/app/(main)/[workspace]/(dashboard)/authors/page.tsx
function Page (line 9) | async function Page() {
FILE: apps/cms/src/app/(main)/[workspace]/(dashboard)/categories/page-client.tsx
function PageClient (line 25) | function PageClient() {
FILE: apps/cms/src/app/(main)/[workspace]/(dashboard)/categories/page.tsx
function Page (line 8) | function Page() {
FILE: apps/cms/src/app/(main)/[workspace]/(dashboard)/layout.tsx
function DashboardLayout (line 19) | async function DashboardLayout({
FILE: apps/cms/src/app/(main)/[workspace]/(dashboard)/loading.tsx
function Loading (line 3) | function Loading() {
FILE: apps/cms/src/app/(main)/[workspace]/(dashboard)/media/[id]/page-client.tsx
type MediaDetailPageProps (line 40) | interface MediaDetailPageProps {
function MediaDetailPage (line 44) | function MediaDetailPage({
function MediaDetailsPanel (line 257) | function MediaDetailsPanel({
function DetailItem (line 339) | function DetailItem({ label, value }: { label: string; value: string }) {
FILE: apps/cms/src/app/(main)/[workspace]/(dashboard)/media/[id]/page.tsx
function Page (line 8) | async function Page(props: {
FILE: apps/cms/src/app/(main)/[workspace]/(dashboard)/media/page-client.tsx
function PageClient (line 22) | function PageClient() {
FILE: apps/cms/src/app/(main)/[workspace]/(dashboard)/media/page.tsx
function Page (line 8) | function Page() {
FILE: apps/cms/src/app/(main)/[workspace]/(dashboard)/posts/page-client.tsx
function PageClient (line 31) | function PageClient() {
FILE: apps/cms/src/app/(main)/[workspace]/(dashboard)/posts/page.tsx
function Page (line 8) | function Page() {
FILE: apps/cms/src/app/(main)/[workspace]/(dashboard)/settings/account/page-client.tsx
function PageClient (line 37) | function PageClient() {
FILE: apps/cms/src/app/(main)/[workspace]/(dashboard)/settings/account/page.tsx
function Page (line 9) | async function Page() {
FILE: apps/cms/src/app/(main)/[workspace]/(dashboard)/settings/appearance/page-client.tsx
function PageClient (line 8) | function PageClient() {
FILE: apps/cms/src/app/(main)/[workspace]/(dashboard)/settings/appearance/page.tsx
function Page (line 9) | async function Page() {
FILE: apps/cms/src/app/(main)/[workspace]/(dashboard)/settings/billing/page-client.tsx
function PageClient (line 21) | function PageClient() {
FILE: apps/cms/src/app/(main)/[workspace]/(dashboard)/settings/billing/page.tsx
function Page (line 9) | async function Page() {
FILE: apps/cms/src/app/(main)/[workspace]/(dashboard)/settings/fields/page-client.tsx
function PageClient (line 36) | function PageClient() {
FILE: apps/cms/src/app/(main)/[workspace]/(dashboard)/settings/fields/page.tsx
function Page (line 8) | async function Page() {
FILE: apps/cms/src/app/(main)/[workspace]/(dashboard)/settings/general/page-client.tsx
function PageClient (line 13) | function PageClient() {
FILE: apps/cms/src/app/(main)/[workspace]/(dashboard)/settings/general/page.tsx
function Page (line 9) | async function Page() {
FILE: apps/cms/src/app/(main)/[workspace]/(dashboard)/settings/keys/page-client.tsx
function PageClient (line 24) | function PageClient() {
FILE: apps/cms/src/app/(main)/[workspace]/(dashboard)/settings/keys/page.tsx
function Page (line 8) | function Page() {
FILE: apps/cms/src/app/(main)/[workspace]/(dashboard)/settings/members/page-client.tsx
function PageClient (line 23) | function PageClient() {
FILE: apps/cms/src/app/(main)/[workspace]/(dashboard)/settings/members/page.tsx
function Page (line 9) | async function Page() {
FILE: apps/cms/src/app/(main)/[workspace]/(dashboard)/settings/notifications/page-client.tsx
function NotificationToggle (line 18) | function NotificationToggle({
type NotificationGroupProps (line 46) | interface NotificationGroupProps {
function NotificationGroup (line 55) | function NotificationGroup({
function PageClient (line 84) | function PageClient() {
FILE: apps/cms/src/app/(main)/[workspace]/(dashboard)/settings/notifications/page.tsx
function Page (line 9) | async function Page() {
FILE: apps/cms/src/app/(main)/[workspace]/(dashboard)/settings/webhooks/page-client.tsx
function PageClient (line 16) | function PageClient() {
FILE: apps/cms/src/app/(main)/[workspace]/(dashboard)/settings/webhooks/page.tsx
function Page (line 8) | async function Page() {
FILE: apps/cms/src/app/(main)/[workspace]/(dashboard)/tags/page-client.tsx
function PageClient (line 23) | function PageClient() {
FILE: apps/cms/src/app/(main)/[workspace]/(dashboard)/tags/page.tsx
function Page (line 8) | function Page() {
FILE: apps/cms/src/app/(main)/[workspace]/(editor)/editor/p/[id]/page-client.tsx
function PageClient (line 7) | function PageClient() {
FILE: apps/cms/src/app/(main)/[workspace]/(editor)/editor/p/[id]/page.tsx
function Page (line 8) | function Page() {
FILE: apps/cms/src/app/(main)/[workspace]/(editor)/editor/p/new/page-client.tsx
function NewPostPageClient (line 6) | function NewPostPageClient() {
FILE: apps/cms/src/app/(main)/[workspace]/(editor)/editor/p/new/page.tsx
function Page (line 8) | function Page() {
FILE: apps/cms/src/app/(main)/[workspace]/(editor)/layout.tsx
function EditorLayout (line 3) | function EditorLayout({ children }: { children: React.ReactNode }) {
FILE: apps/cms/src/app/(main)/[workspace]/layout.tsx
function WorkspaceLayout (line 11) | async function WorkspaceLayout({
FILE: apps/cms/src/app/(main)/[workspace]/loading.tsx
function Loading (line 3) | function Loading() {
FILE: apps/cms/src/app/(main)/[workspace]/set-workspace-cookie.tsx
function SetWorkspaceCookie (line 6) | function SetWorkspaceCookie({
FILE: apps/cms/src/app/(main)/layout.tsx
function MainLayout (line 3) | async function MainLayout({
FILE: apps/cms/src/app/(share)/layout.tsx
function ShareLayout (line 1) | function ShareLayout({
FILE: apps/cms/src/app/(share)/share/[token]/page-client.tsx
function SharePageClient (line 14) | function SharePageClient({ data, status }: SharePageClientProps) {
FILE: apps/cms/src/app/(share)/share/[token]/page.tsx
type SharePageProps (line 12) | interface SharePageProps {
function fetchShareData (line 16) | async function fetchShareData(token: string) {
function SharePage (line 51) | async function SharePage(props: SharePageProps) {
FILE: apps/cms/src/app/api/accounts/[id]/route.ts
function DELETE (line 5) | async function DELETE(
FILE: apps/cms/src/app/api/accounts/route.ts
function GET (line 5) | async function GET() {
FILE: apps/cms/src/app/api/ai/suggestions/prompt.ts
type SystemPromptParams (line 1) | interface SystemPromptParams {
FILE: apps/cms/src/app/api/ai/suggestions/route.tsx
function createContentHash (line 16) | function createContentHash(
function POST (line 45) | async function POST(request: Request) {
FILE: apps/cms/src/app/api/authors/[id]/route.ts
function DELETE (line 12) | async function DELETE(
function PATCH (line 77) | async function PATCH(
FILE: apps/cms/src/app/api/authors/route.ts
function GET (line 13) | async function GET() {
function POST (line 63) | async function POST(request: Request) {
FILE: apps/cms/src/app/api/categories/[id]/route.ts
function PATCH (line 12) | async function PATCH(
function DELETE (line 79) | async function DELETE(
FILE: apps/cms/src/app/api/categories/route.ts
function GET (line 12) | async function GET() {
function POST (line 47) | async function POST(req: Request) {
FILE: apps/cms/src/app/api/fields/[id]/route.ts
function buildFieldOptionWrites (line 7) | function buildFieldOptionWrites(
function areFieldOptionsEqual (line 17) | function areFieldOptionsEqual(
function isUniqueConstraintError (line 35) | function isUniqueConstraintError(error: unknown) {
function isTransactionConflict (line 58) | function isTransactionConflict(error: unknown) {
function PATCH (line 66) | async function PATCH(
function DELETE (line 248) | async function DELETE(
FILE: apps/cms/src/app/api/fields/route.ts
function buildFieldOptionWrites (line 7) | function buildFieldOptionWrites(
function isUniqueFieldKeyConflict (line 17) | function isUniqueFieldKeyConflict(error: unknown) {
function GET (line 40) | async function GET() {
function POST (line 74) | async function POST(req: Request) {
FILE: apps/cms/src/app/api/keys/[id]/route.ts
function GET (line 7) | async function GET(
function PATCH (line 49) | async function PATCH(
function DELETE (line 130) | async function DELETE(
FILE: apps/cms/src/app/api/keys/route.ts
function GET (line 8) | async function GET() {
function POST (line 38) | async function POST(request: Request) {
FILE: apps/cms/src/app/api/media/[id]/route.ts
function GET (line 11) | async function GET(
function PATCH (line 67) | async function PATCH(
FILE: apps/cms/src/app/api/media/editor/route.ts
function GET (line 8) | async function GET(request: Request) {
FILE: apps/cms/src/app/api/media/route.ts
function GET (line 16) | async function GET(request: Request) {
function DELETE (line 95) | async function DELETE(request: Request) {
FILE: apps/cms/src/app/api/metrics/publishing/route.ts
function GET (line 6) | async function GET() {
FILE: apps/cms/src/app/api/metrics/usage/route.ts
constant CHART_DAYS (line 7) | const CHART_DAYS = 30;
function GET (line 9) | async function GET() {
FILE: apps/cms/src/app/api/polar/success/route.ts
function GET (line 8) | async function GET(request: Request) {
FILE: apps/cms/src/app/api/posts/[id]/fields/route.ts
function GET (line 10) | async function GET(
function PUT (line 63) | async function PUT(
FILE: apps/cms/src/app/api/posts/[id]/route.ts
function buildCustomFieldWrites (line 15) | async function buildCustomFieldWrites(
function GET (line 42) | async function GET(
function PATCH (line 100) | async function PATCH(
function DELETE (line 309) | async function DELETE(
FILE: apps/cms/src/app/api/posts/import/route.ts
function POST (line 16) | async function POST(request: Request) {
FILE: apps/cms/src/app/api/posts/route.ts
function buildCustomFieldWrites (line 19) | async function buildCustomFieldWrites(
constant POST_SORT_FIELDS (line 55) | const POST_SORT_FIELDS = new Set([
function splitPostSort (line 62) | function splitPostSort(sort: string) {
function GET (line 70) | async function GET(request: Request) {
function POST (line 149) | async function POST(request: Request) {
FILE: apps/cms/src/app/api/share/[token]/route.ts
constant NO_STORE_HEADERS (line 4) | const NO_STORE_HEADERS = {
function GET (line 8) | async function GET(
FILE: apps/cms/src/app/api/share/route.ts
function POST (line 8) | async function POST(request: Request) {
FILE: apps/cms/src/app/api/tags/[id]/route.ts
function parseTagRequest (line 12) | async function parseTagRequest(req: Request) {
function PATCH (line 20) | async function PATCH(
function DELETE (line 89) | async function DELETE(
FILE: apps/cms/src/app/api/tags/route.ts
function GET (line 12) | async function GET() {
function POST (line 47) | async function POST(req: Request) {
FILE: apps/cms/src/app/api/upload/complete/route.ts
function POST (line 14) | async function POST(request: Request) {
FILE: apps/cms/src/app/api/upload/route.ts
function POST (line 10) | async function POST(request: Request) {
FILE: apps/cms/src/app/api/user/notifications/route.ts
function getNotificationPreferences (line 6) | async function getNotificationPreferences(
function GET (line 45) | async function GET() {
function PATCH (line 59) | async function PATCH(request: Request) {
FILE: apps/cms/src/app/api/user/route.ts
function GET (line 5) | async function GET() {
function PATCH (line 57) | async function PATCH(request: Request) {
FILE: apps/cms/src/app/api/webhooks/[id]/route.ts
function PATCH (line 11) | async function PATCH(
function DELETE (line 98) | async function DELETE(
FILE: apps/cms/src/app/api/webhooks/[id]/test/route.ts
function POST (line 5) | async function POST(
FILE: apps/cms/src/app/api/webhooks/route.ts
function GET (line 7) | async function GET() {
function POST (line 28) | async function POST(req: Request) {
FILE: apps/cms/src/app/api/workspaces/[slug]/route.ts
function GET (line 6) | async function GET(
FILE: apps/cms/src/app/api/workspaces/route.ts
function GET (line 6) | async function GET() {
FILE: apps/cms/src/app/layout.tsx
function DatabuddyAnalytics (line 49) | function DatabuddyAnalytics() {
function RootLayout (line 62) | async function RootLayout({
FILE: apps/cms/src/app/not-found.tsx
function NotFound (line 7) | function NotFound() {
FILE: apps/cms/src/app/providers.tsx
function Providers (line 19) | function Providers({ children }: { children: React.ReactNode }) {
FILE: apps/cms/src/app/robots.ts
function robots (line 3) | function robots(): MetadataRoute.Robots {
FILE: apps/cms/src/components/auth/login-form.tsx
function LoginForm (line 22) | function LoginForm() {
FILE: apps/cms/src/components/auth/register-form.tsx
function RegisterForm (line 22) | function RegisterForm() {
FILE: apps/cms/src/components/auth/reset/reset-form.tsx
type ResetFormProps (line 12) | interface ResetFormProps {
function ResetForm (line 17) | function ResetForm({ callbackUrl, token }: ResetFormProps) {
FILE: apps/cms/src/components/auth/reset/reset-request-form.tsx
function ResetRequestForm (line 11) | function ResetRequestForm() {
FILE: apps/cms/src/components/auth/verify-form.tsx
type VerifyFormProps (line 19) | interface VerifyFormProps {
function VerifyForm (line 24) | function VerifyForm({ email, callbackUrl }: VerifyFormProps) {
FILE: apps/cms/src/components/authors/author-sheet.tsx
type AuthorSheetProps (line 48) | interface AuthorSheetProps {
FILE: apps/cms/src/components/authors/data-table.tsx
type AuthorDataTableProps (line 34) | interface AuthorDataTableProps {
function AuthorDataTable (line 39) | function AuthorDataTable({ columns, data }: AuthorDataTableProps) {
FILE: apps/cms/src/components/authors/table-actions.tsx
type AuthorTableActionsProps (line 21) | interface AuthorTableActionsProps {
function AuthorTableActions (line 25) | function AuthorTableActions({ author }: AuthorTableActionsProps) {
FILE: apps/cms/src/components/billing/success-modal.tsx
type PaymentSuccessModalProps (line 15) | interface PaymentSuccessModalProps {
function PaymentSuccessModal (line 21) | function PaymentSuccessModal({
FILE: apps/cms/src/components/billing/upgrade-modal.tsx
type FeatureType (line 22) | type FeatureType = "authors" | "share-drafts" | "team-members" | "storage";
constant FEATURE_CONTENT (line 24) | const FEATURE_CONTENT: Record<
type UpgradeModalProps (line 49) | interface UpgradeModalProps {
function UpgradeModal (line 55) | function UpgradeModal({ feature, isOpen, onClose }: UpgradeModalProps) {
FILE: apps/cms/src/components/categories/columns.tsx
type Category (line 6) | interface Category {
FILE: apps/cms/src/components/categories/data-table.tsx
type DataTableProps (line 26) | interface DataTableProps<TData, TValue> {
function DataTable (line 31) | function DataTable<TData, TValue>({
FILE: apps/cms/src/components/categories/table-actions.tsx
function TableActions (line 18) | function TableActions(props: Category) {
FILE: apps/cms/src/components/editor/ai/readability-suggestions.tsx
type ReadabilitySuggestion (line 8) | interface ReadabilitySuggestion {
type ReadabilitySuggestionsProps (line 14) | interface ReadabilitySuggestionsProps {
function highlightTextInEditor (line 21) | function highlightTextInEditor(editor: Editor, textReference: string) {
function ReadabilitySuggestionsBase (line 56) | function ReadabilitySuggestionsBase({
FILE: apps/cms/src/components/editor/editor-data-provider.tsx
type EditorMode (line 27) | type EditorMode = "create" | "update";
type EditorBootstrap (line 29) | interface EditorBootstrap {
type EditorDataContextValue (line 34) | interface EditorDataContextValue {
constant CORE_FIELD_LABELS (line 49) | const CORE_FIELD_LABELS: Record<string, string> = {
function buildEditorValues (line 60) | function buildEditorValues(
function buildCustomFieldPayload (line 83) | function buildCustomFieldPayload(
function fetchEditorBootstrap (line 95) | async function fetchEditorBootstrap(
function EditorDataProvider (line 146) | function EditorDataProvider({
function useEditorData (line 365) | function useEditorData() {
FILE: apps/cms/src/components/editor/editor-header.tsx
type EditorHeaderProps (line 24) | interface EditorHeaderProps {
function EditorHeader (line 29) | function EditorHeader({ postId, workspace }: EditorHeaderProps) {
FILE: apps/cms/src/components/editor/editor-page.tsx
function EditorPageContent (line 32) | function EditorPageContent() {
function EditorPage (line 235) | function EditorPage() {
FILE: apps/cms/src/components/editor/editor-sidebar.tsx
type EditorSidebarProps (line 45) | type EditorSidebarProps = React.ComponentProps<typeof Sidebar>;
function EditorSidebar (line 47) | function EditorSidebar({ ...props }: EditorSidebarProps) {
FILE: apps/cms/src/components/editor/editor.tsx
function MarbleEditorMenus (line 37) | function MarbleEditorMenus() {
FILE: apps/cms/src/components/editor/fields/author-selector.tsx
type AuthorOptions (line 45) | interface AuthorOptions {
type AuthorSelectorProps (line 52) | interface AuthorSelectorProps<TFieldValues extends FieldValues> {
constant EMPTY_AUTHORS (line 60) | const EMPTY_AUTHORS: string[] = [];
function AuthorSelector (line 62) | function AuthorSelector<TFieldValues extends FieldValues>({
FILE: apps/cms/src/components/editor/fields/category-selector.tsx
type CategoryResponse (line 26) | interface CategoryResponse {
type CategorySelectorProps (line 32) | interface CategorySelectorProps<TFieldValues extends FieldValues> {
function CategorySelector (line 36) | function CategorySelector<TFieldValues extends FieldValues>({
FILE: apps/cms/src/components/editor/fields/cover-image-selector.tsx
type CoverImageSelectorProps (line 57) | interface CoverImageSelectorProps<TFieldValues extends FieldValues> {
function CoverImageSelector (line 61) | function CoverImageSelector<TFieldValues extends FieldValues>({
FILE: apps/cms/src/components/editor/fields/custom-fields-section.tsx
function CustomFieldsSection (line 50) | function CustomFieldsSection() {
function parseMultiselectValue (line 74) | function parseMultiselectValue(value: string | null | undefined) {
function getCustomFieldControlId (line 89) | function getCustomFieldControlId(fieldId: string) {
function getCustomFieldLabelId (line 93) | function getCustomFieldLabelId(fieldId: string) {
function FieldLabel (line 97) | function FieldLabel({
function FieldInput (line 117) | function FieldInput({ field }: { field: CustomField }) {
function MultiselectField (line 319) | function MultiselectField({
FILE: apps/cms/src/components/editor/fields/description-field.tsx
type DescriptionFieldProps (line 14) | interface DescriptionFieldProps<TFieldValues extends FieldValues> {
function DescriptionField (line 18) | function DescriptionField<TFieldValues extends FieldValues>({
FILE: apps/cms/src/components/editor/fields/featured-field.tsx
type FeaturedFieldProps (line 13) | interface FeaturedFieldProps<TFieldValues extends FieldValues> {
function FeaturedField (line 17) | function FeaturedField<TFieldValues extends FieldValues>({
FILE: apps/cms/src/components/editor/fields/field-info.tsx
type FieldInfoProps (line 8) | interface FieldInfoProps {
function FieldInfo (line 19) | function FieldInfo({
FILE: apps/cms/src/components/editor/fields/publish-date-field.tsx
type PublishDateFieldProps (line 23) | interface PublishDateFieldProps<TFieldValues extends FieldValues> {
function toUTCMidnight (line 27) | function toUTCMidnight(date: Date) {
function utcToLocalDate (line 33) | function utcToLocalDate(date: Date) {
function PublishDateField (line 37) | function PublishDateField<TFieldValues extends FieldValues>({
FILE: apps/cms/src/components/editor/fields/slug-field.tsx
type SlugFieldProps (line 16) | interface SlugFieldProps<TFieldValues extends FieldValues> {
function SlugField (line 20) | function SlugField<TFieldValues extends FieldValues>({
FILE: apps/cms/src/components/editor/fields/status-field.tsx
type StatusFieldProps (line 13) | interface StatusFieldProps<TFieldValues extends FieldValues> {
function StatusField (line 17) | function StatusField<TFieldValues extends FieldValues>({
FILE: apps/cms/src/components/editor/fields/tag-selector.tsx
type Option (line 41) | interface Option {
type TagResponse (line 47) | interface TagResponse {
type MultiSelectPopoverProps (line 53) | interface MultiSelectPopoverProps<TFieldValues extends FieldValues> {
constant EMPTY_TAGS (line 61) | const EMPTY_TAGS: string[] = [];
FILE: apps/cms/src/components/editor/footer/metadata-footer.tsx
type MetadataFooterProps (line 4) | interface MetadataFooterProps {
function MetadataFooter (line 8) | function MetadataFooter({ isSubmitting }: MetadataFooterProps) {
FILE: apps/cms/src/components/editor/link-selector.tsx
function isValidUrl (line 15) | function isValidUrl(url: string) {
function getUrlFromString (line 24) | function getUrlFromString(str: string) {
type LinkSelectorProps (line 37) | interface LinkSelectorProps {
FILE: apps/cms/src/components/editor/share-modal.tsx
type ShareModalProps (line 28) | interface ShareModalProps {
function ShareModal (line 32) | function ShareModal({ postId }: ShareModalProps) {
FILE: apps/cms/src/components/editor/tabs/analysis-tab.tsx
type AnalysisTabProps (line 19) | interface AnalysisTabProps {
function AnalysisTab (line 25) | function AnalysisTab({
FILE: apps/cms/src/components/editor/tabs/metadata-tab.tsx
type MetadataTabProps (line 18) | interface MetadataTabProps {
function MetadataTab (line 23) | function MetadataTab({ initialAuthors, tags }: MetadataTabProps) {
FILE: apps/cms/src/components/editor/textarea-autosize.tsx
type ExtendedTextareaAutosizeProps (line 11) | type ExtendedTextareaAutosizeProps = TextareaAutosizeProps & {
function TextareaAutosize (line 15) | function TextareaAutosize({
FILE: apps/cms/src/components/fields/create-custom-field.tsx
function toSnakeCase (line 50) | function toSnakeCase(str: string): string {
type CreateCustomFieldSheetProps (line 57) | interface CreateCustomFieldSheetProps {
function CreateCustomFieldSheet (line 61) | function CreateCustomFieldSheet({ children }: CreateCustomFieldSheetProp...
FILE: apps/cms/src/components/fields/custom-field-row.tsx
type CustomFieldRowProps (line 20) | interface CustomFieldRowProps {
function CustomFieldRow (line 26) | function CustomFieldRow({
FILE: apps/cms/src/components/fields/delete-custom-field.tsx
type DeleteCustomFieldModalProps (line 23) | interface DeleteCustomFieldModalProps {
function DeleteCustomFieldModal (line 31) | function DeleteCustomFieldModal({
FILE: apps/cms/src/components/fields/edit-custom-field.tsx
type EditCustomFieldSheetProps (line 48) | interface EditCustomFieldSheetProps {
function EditCustomFieldSheet (line 54) | function EditCustomFieldSheet({
FILE: apps/cms/src/components/fields/field-options-input.tsx
type FieldOptionsInputProps (line 16) | interface FieldOptionsInputProps {
function FieldOptionsInput (line 25) | function FieldOptionsInput({
FILE: apps/cms/src/components/home/api-usage-card.tsx
type ApiUsageCardProps (line 19) | interface ApiUsageCardProps {
function ApiUsageCard (line 26) | function ApiUsageCard({ data, isLoading }: ApiUsageCardProps) {
FILE: apps/cms/src/components/home/media-usage-card.tsx
type MediaUsageCardProps (line 22) | interface MediaUsageCardProps {
function getMediaTypeIcon (line 27) | function getMediaTypeIcon(type: string) {
function MediaUsageCard (line 40) | function MediaUsageCard({ data, isLoading }: MediaUsageCardProps) {
FILE: apps/cms/src/components/home/webhook-usage-card.tsx
type WebhookUsageCardProps (line 19) | interface WebhookUsageCardProps {
function WebhookUsageCard (line 26) | function WebhookUsageCard({ data, isLoading }: WebhookUsageCardProps) {
FILE: apps/cms/src/components/icons/marble.tsx
function MarbleIcon (line 1) | function MarbleIcon() {
FILE: apps/cms/src/components/icons/social/index.tsx
type SocialIconProps (line 3) | interface SocialIconProps extends SVGProps<SVGSVGElement> {}
FILE: apps/cms/src/components/invoice/columns.tsx
type Invoice (line 8) | interface Invoice {
FILE: apps/cms/src/components/invoice/data-table.tsx
type DataTableProps (line 23) | interface DataTableProps<TData, TValue> {
function InvoiceDataTable (line 28) | function InvoiceDataTable<TData, TValue>({
function getHeaderClassName (line 119) | function getHeaderClassName(columnId: string) {
function getCellClassName (line 134) | function getCellClassName(columnId: string) {
FILE: apps/cms/src/components/invoice/table-actions.tsx
function TableActions (line 16) | function TableActions(props: Invoice) {
FILE: apps/cms/src/components/keys/api-key-modal.tsx
type ApiKeyModalProps (line 42) | interface ApiKeyModalProps {
function ApiKeyModal (line 49) | function ApiKeyModal({ data, mode, open, setOpen }: ApiKeyModalProps) {
FILE: apps/cms/src/components/keys/columns.tsx
type APIKey (line 9) | interface APIKey {
FILE: apps/cms/src/components/keys/data-table.tsx
type DataTableProps (line 31) | interface DataTableProps<TData, TValue> {
function DataTable (line 36) | function DataTable<TData, TValue>({
FILE: apps/cms/src/components/keys/table-actions.tsx
function TableActions (line 19) | function TableActions(props: APIKey) {
FILE: apps/cms/src/components/layout/header-sidebar-trigger.tsx
function HeaderSidebarTrigger (line 22) | function HeaderSidebarTrigger() {
FILE: apps/cms/src/components/layout/wrapper.tsx
type DashboardBodySize (line 7) | type DashboardBodySize = "default" | "compact" | "wide";
type DashboardBodyProps (line 9) | interface DashboardBodyProps {
function getBodySizeClassName (line 22) | function getBodySizeClassName(size: DashboardBodySize) {
function DashboardBody (line 33) | function DashboardBody({
FILE: apps/cms/src/components/media/crop-image-modal.tsx
type Props (line 23) | interface Props {
function CropImageModal (line 34) | function CropImageModal({
constant DATA_URL_REGEX (line 115) | const DATA_URL_REGEX = /^data:(.+?);base64,(.*)$/;
function dataUrlToFile (line 117) | function dataUrlToFile(dataUrl: string, filename: string): File {
FILE: apps/cms/src/components/media/delete-modal.tsx
type DeleteMediaModalProps (line 27) | interface DeleteMediaModalProps {
function removeDeletedMediaFromPage (line 34) | function removeDeletedMediaFromPage(
function DeleteMediaModal (line 58) | function DeleteMediaModal({
FILE: apps/cms/src/components/media/file-upload-input.tsx
type FileUploadInputProps (line 9) | interface FileUploadInputProps {
function FileUploadInput (line 20) | function FileUploadInput({
FILE: apps/cms/src/components/media/media-actions.tsx
type MediaActionsProps (line 20) | interface MediaActionsProps {
function copyMediaUrl (line 25) | async function copyMediaUrl(url: string) {
FILE: apps/cms/src/components/media/media-card.tsx
type MediaCardProps (line 29) | interface MediaCardProps {
function MediaCard (line 43) | function MediaCard({
FILE: apps/cms/src/components/media/media-columns.tsx
type MediaColumnsOptions (line 19) | interface MediaColumnsOptions {
function getMediaTypeLabel (line 30) | function getMediaTypeLabel(media: Media) {
function getMediaDimensions (line 34) | function getMediaDimensions(media: Media) {
function getMediaColumns (line 83) | function getMediaColumns({
FILE: apps/cms/src/components/media/media-controls.tsx
function MediaControls (line 39) | function MediaControls({
FILE: apps/cms/src/components/media/media-data-table.tsx
type MediaDataTableProps (line 42) | interface MediaDataTableProps {
function sortToSortingState (line 53) | function sortToSortingState(sort: MediaSort): SortingState {
function sortingStateToSort (line 58) | function sortingStateToSort(sorting: SortingState): MediaSort {
function MediaDataTable (line 68) | function MediaDataTable({
function shouldIgnoreRowClick (line 333) | function shouldIgnoreRowClick(event: MouseEvent) {
function getHeaderClassName (line 345) | function getHeaderClassName(columnId: string) {
function getCellClassName (line 363) | function getCellClassName(columnId: string) {
FILE: apps/cms/src/components/media/media-gallery.tsx
type MediaGalleryProps (line 14) | interface MediaGalleryProps {
function MediaGallery (line 33) | function MediaGallery({
FILE: apps/cms/src/components/media/media-table-toolbar.tsx
type MediaTableToolbarProps (line 39) | interface MediaTableToolbarProps {
type MediaViewType (line 48) | type MediaViewType = "table" | "grid";
function MediaTableToolbar (line 80) | function MediaTableToolbar({
FILE: apps/cms/src/components/media/upload-modal.tsx
type MediaUploadModalProps (line 25) | interface MediaUploadModalProps {
function MediaUploadModal (line 31) | function MediaUploadModal({
FILE: apps/cms/src/components/media/video-player.tsx
type VideoPlayerProps (line 7) | type VideoPlayerProps = VideoHTMLAttributes<HTMLVideoElement> & {
FILE: apps/cms/src/components/nav/announcements.tsx
function Announcements (line 11) | function Announcements() {
FILE: apps/cms/src/components/nav/app-breadcrumb.tsx
function AppBreadcrumb (line 21) | function AppBreadcrumb() {
FILE: apps/cms/src/components/nav/app-sidebar.tsx
function AppSidebar (line 42) | function AppSidebar({ ...props }: React.ComponentProps<typeof Sidebar>) {
function SidebarCollapseTrigger (line 174) | function SidebarCollapseTrigger() {
FILE: apps/cms/src/components/nav/create-workspace-dialog.tsx
type CreateWorkspaceDialogProps (line 33) | interface CreateWorkspaceDialogProps {
function onSubmit (line 56) | async function onSubmit(payload: CreateWorkspaceValues) {
FILE: apps/cms/src/components/nav/nav-extra.tsx
type NavExtraProps (line 56) | interface NavExtraProps {
function NavExtra (line 60) | function NavExtra({ asMenuButton = false }: NavExtraProps) {
FILE: apps/cms/src/components/nav/nav-main.tsx
function NavMain (line 51) | function NavMain() {
FILE: apps/cms/src/components/nav/nav-settings.tsx
function NavSettings (line 80) | function NavSettings() {
FILE: apps/cms/src/components/nav/nav-user.tsx
function NavUser (line 24) | function NavUser() {
FILE: apps/cms/src/components/nav/sidebar-footer-content.tsx
function SidebarFooterContent (line 8) | function SidebarFooterContent() {
FILE: apps/cms/src/components/nav/theme-toggle.tsx
type Theme (line 14) | type Theme = (typeof themes)[number];
function ThemeToggle (line 20) | function ThemeToggle() {
FILE: apps/cms/src/components/nav/upgrade-card.tsx
function UpgradeCard (line 12) | function UpgradeCard() {
FILE: apps/cms/src/components/nav/whats-new-card.tsx
function WhatsNewCard (line 15) | function WhatsNewCard() {
FILE: apps/cms/src/components/nav/workspace-switcher.tsx
function WorkspaceSwitcher (line 36) | function WorkspaceSwitcher() {
FILE: apps/cms/src/components/posts/columns.tsx
type Post (line 12) | interface Post {
FILE: apps/cms/src/components/posts/data-grid.tsx
type DataGridProps (line 29) | interface DataGridProps {
function DataGrid (line 33) | function DataGrid({ data }: DataGridProps) {
FILE: apps/cms/src/components/posts/data-table.tsx
type DataTableProps (line 21) | interface DataTableProps<TData> {
function DataTable (line 26) | function DataTable<TData>({ table, rows }: DataTableProps<TData>) {
function getHeaderClassName (line 119) | function getHeaderClassName(columnId: string) {
function getCellClassName (line 135) | function getCellClassName(columnId: string) {
FILE: apps/cms/src/components/posts/data-view.tsx
type Category (line 56) | interface Category {
type DataViewProps (line 79) | interface DataViewProps<TData, TValue> {
type ViewType (line 87) | type ViewType = "table" | "grid";
function PostDataView (line 89) | function PostDataView<TData, TValue>({
FILE: apps/cms/src/components/posts/import-item-form.tsx
type ImportItemFormProps (line 24) | interface ImportItemFormProps {
function isFormValid (line 31) | function isFormValid(values: Partial<PostValues>): boolean {
function ImportItemForm (line 42) | function ImportItemForm({
FILE: apps/cms/src/components/posts/import-modal.tsx
type ImportState (line 37) | interface ImportState {
function PostsImportModal (line 44) | function PostsImportModal({
FILE: apps/cms/src/components/posts/post-actions.tsx
type PostTableActionsProps (line 21) | interface PostTableActionsProps {
function PostActions (line 26) | function PostActions({
FILE: apps/cms/src/components/settings/account.tsx
type AccountFormProps (line 15) | interface AccountFormProps {
function AccountForm (line 20) | function AccountForm({ name, email }: AccountFormProps) {
FILE: apps/cms/src/components/settings/delete-account.tsx
function DeleteAccountModal (line 23) | function DeleteAccountModal() {
FILE: apps/cms/src/components/settings/fields/delete.tsx
function Delete (line 31) | function Delete() {
FILE: apps/cms/src/components/settings/fields/id.tsx
function Id (line 10) | function Id() {
FILE: apps/cms/src/components/settings/fields/logo.tsx
function Logo (line 26) | function Logo() {
FILE: apps/cms/src/components/settings/fields/name.tsx
function Name (line 20) | function Name() {
FILE: apps/cms/src/components/settings/fields/slug.tsx
function Slug (line 20) | function Slug() {
FILE: apps/cms/src/components/settings/fields/timezone.tsx
function Timezone (line 23) | function Timezone() {
FILE: apps/cms/src/components/settings/section.tsx
function SettingsSection (line 4) | function SettingsSection({
FILE: apps/cms/src/components/settings/theme.tsx
function ThemeSwitch (line 28) | function ThemeSwitch() {
FILE: apps/cms/src/components/share/prose.tsx
type ProseProps (line 4) | type ProseProps = React.HTMLAttributes<HTMLElement> & {
function Prose (line 9) | function Prose({ children, html, className }: ProseProps) {
FILE: apps/cms/src/components/share/screens.tsx
function LinkExpired (line 9) | function LinkExpired() {
function LinkNotFound (line 27) | function LinkNotFound() {
FILE: apps/cms/src/components/shared/container.tsx
type ContainerProps (line 3) | interface ContainerProps extends React.HTMLAttributes<HTMLDivElement> {
function Container (line 7) | function Container({ className, children, ...props }: ContainerProps) {
FILE: apps/cms/src/components/shared/dropzone.tsx
type DropzoneProps (line 9) | interface DropzoneProps {
function Dropzone (line 24) | function Dropzone({
type MediaDropzoneProps (line 116) | interface MediaDropzoneProps
constant EMPTY_PLACEHOLDER (line 125) | const EMPTY_PLACEHOLDER: MediaDropzoneProps["placeholder"] = {};
function MediaDropzone (line 127) | function MediaDropzone({
function ImageDropzone (line 147) | function ImageDropzone({
FILE: apps/cms/src/components/shared/page-loader.tsx
function PageLoader (line 5) | function PageLoader() {
FILE: apps/cms/src/components/shared/pending-state.tsx
type PendingStateProps (line 3) | type PendingStateProps = PropsWithChildren<{
function PendingState (line 10) | function PendingState({
FILE: apps/cms/src/components/tags/columns.tsx
type Tag (line 6) | interface Tag {
FILE: apps/cms/src/components/tags/data-table.tsx
type DataTableProps (line 26) | interface DataTableProps<TData, TValue> {
function DataTable (line 31) | function DataTable<TData, TValue>({
FILE: apps/cms/src/components/tags/table-actions.tsx
function TableActions (line 18) | function TableActions(props: Tag) {
FILE: apps/cms/src/components/tags/tag-modals.tsx
function TagModal (line 42) | function TagModal({
FILE: apps/cms/src/components/team/columns.tsx
type UserRole (line 12) | type UserRole = "owner" | "admin" | "member";
type TeamMemberRow (line 14) | interface TeamMemberRow {
FILE: apps/cms/src/components/team/data-table.tsx
type UserRole (line 31) | type UserRole = "owner" | "admin" | "member" | undefined;
type DataTableProps (line 33) | interface DataTableProps<TData, TValue> {
function TeamDataTable (line 42) | function TeamDataTable<TData, TValue>({
FILE: apps/cms/src/components/team/invite-button.tsx
type InviteButtonProps (line 15) | interface InviteButtonProps {
function InviteButton (line 19) | function InviteButton({ onInvite }: InviteButtonProps) {
FILE: apps/cms/src/components/team/invite-section.tsx
type Invite (line 23) | interface Invite {
type InviteSectionProps (line 32) | interface InviteSectionProps {
function InviteSection (line 36) | function InviteSection({ invitations }: InviteSectionProps) {
FILE: apps/cms/src/components/team/leave-workspace.tsx
type ListOrganizationResponse (line 23) | interface ListOrganizationResponse {
type LeaveWorkspaceModalProps (line 33) | interface LeaveWorkspaceModalProps {
function LeaveWorkspaceModal (line 40) | function LeaveWorkspaceModal({
FILE: apps/cms/src/components/team/profile-sheet.tsx
type ProfileSheetProps (line 30) | interface ProfileSheetProps {
function ProfileSheet (line 36) | function ProfileSheet({ open, setOpen, member }: ProfileSheetProps) {
FILE: apps/cms/src/components/team/table-actions.tsx
type TableActionsProps (line 18) | interface TableActionsProps extends TeamMemberRow {
function TableActions (line 23) | function TableActions(props: TableActionsProps) {
FILE: apps/cms/src/components/team/team-modals.tsx
type RemoveMemberModalProps (line 23) | interface RemoveMemberModalProps {
function RemoveMemberModal (line 29) | function RemoveMemberModal({
FILE: apps/cms/src/components/ui/async-button.tsx
type AsyncButtonProps (line 9) | type AsyncButtonProps = React.ComponentPropsWithRef<"button"> &
FILE: apps/cms/src/components/ui/copy-button.tsx
type ButtonProps (line 12) | type ButtonProps = React.ComponentProps<"button"> &
function CopyButton (line 15) | function CopyButton({
FILE: apps/cms/src/components/ui/data-table-pagination.tsx
type DataTablePaginationProps (line 9) | interface DataTablePaginationProps {
function DataTablePagination (line 23) | function DataTablePagination({
FILE: apps/cms/src/components/ui/error-message.tsx
type ErrorMessageProps (line 3) | interface ErrorMessageProps
FILE: apps/cms/src/components/ui/gauge.tsx
type GaugeProps (line 8) | interface GaugeProps {
function Gauge (line 24) | function Gauge({
FILE: apps/cms/src/components/ui/hidden-scrollbar.tsx
type HiddenScrollbarProps (line 4) | interface HiddenScrollbarProps extends React.HTMLAttributes<HTMLDivEleme...
function HiddenScrollbar (line 8) | function HiddenScrollbar({
FILE: apps/cms/src/components/ui/last-used-badge.tsx
type LastUsedBadgeProps (line 4) | interface LastUsedBadgeProps extends HTMLAttributes<HTMLSpanElement> {
function LastUsedBadge (line 11) | function LastUsedBadge({
FILE: apps/cms/src/components/ui/loading-spinner.tsx
function LoadingSpinner (line 8) | function LoadingSpinner({ className }: { className?: string }) {
function ButtonLoadingSpinner (line 31) | function ButtonLoadingSpinner({
FILE: apps/cms/src/components/ui/segmented-progress.tsx
type SegmentedProgressProps (line 5) | interface SegmentedProgressProps {
function SegmentedProgress (line 16) | function SegmentedProgress({
FILE: apps/cms/src/components/ui/timezone-selector.tsx
type TimezoneOption (line 25) | interface TimezoneOption {
type TimezoneSelectorProps (line 33) | interface TimezoneSelectorProps {
function TimezoneSelector (line 41) | function TimezoneSelector({
FILE: apps/cms/src/components/webhooks/create-webhook.tsx
type CreateWebhookSheetProps (line 74) | interface CreateWebhookSheetProps {
function CreateWebhookSheet (line 78) | function CreateWebhookSheet({ children }: CreateWebhookSheetProps) {
FILE: apps/cms/src/components/webhooks/delete-webhook.tsx
type DeleteWebhookModalProps (line 22) | interface DeleteWebhookModalProps {
function DeleteWebhookModal (line 30) | function DeleteWebhookModal({
FILE: apps/cms/src/components/webhooks/edit-webhook.tsx
type EditWebhookSheetProps (line 73) | interface EditWebhookSheetProps {
function EditWebhookSheet (line 79) | function EditWebhookSheet({
FILE: apps/cms/src/components/webhooks/webhook-actions.tsx
type WebhookActionsProps (line 38) | interface WebhookActionsProps {
function WebhookActions (line 45) | function WebhookActions({
FILE: apps/cms/src/components/webhooks/webhook-card.tsx
type WebhookCardProps (line 44) | interface WebhookCardProps {
function WebhookCard (line 52) | function WebhookCard({
FILE: apps/cms/src/components/webhooks/webhook-columns.tsx
type WebhookColumnsOptions (line 10) | interface WebhookColumnsOptions {
function formatWebhookFormat (line 17) | function formatWebhookFormat(formatValue: string) {
function getWebhookColumns (line 21) | function getWebhookColumns({
FILE: apps/cms/src/components/webhooks/webhook-data-table.tsx
type WebhookDataTableProps (line 29) | interface WebhookDataTableProps {
function WebhookDataTable (line 37) | function WebhookDataTable({
function WebhooksEmptyState (line 210) | function WebhooksEmptyState() {
function getHeaderClassName (line 232) | function getHeaderClassName(columnId: string) {
function getCellClassName (line 253) | function getCellClassName(columnId: string) {
FILE: apps/cms/src/hooks/use-debounce.ts
function useDebounce (line 3) | function useDebounce<T>(value: T, delay: number): T {
FILE: apps/cms/src/hooks/use-localstorage.ts
function getItemFromLocalStorage (line 3) | function getItemFromLocalStorage(key: string) {
function useLocalStorage (line 16) | function useLocalStorage<T>(
FILE: apps/cms/src/hooks/use-media-actions.ts
function useMediaActions (line 6) | function useMediaActions(mediaQueryKey: MediaQueryKey) {
FILE: apps/cms/src/hooks/use-media-query.ts
function getDevice (line 3) | function getDevice(): "mobile" | "tablet" | "desktop" | null {
function getDimensions (line 15) | function getDimensions() {
function useMediaQuery (line 23) | function useMediaQuery() {
FILE: apps/cms/src/hooks/use-mobile.tsx
constant MOBILE_BREAKPOINT (line 3) | const MOBILE_BREAKPOINT = 768;
function subscribe (line 5) | function subscribe(callback: () => void) {
function getSnapshot (line 11) | function getSnapshot() {
function getServerSnapshot (line 15) | function getServerSnapshot() {
function useIsMobile (line 19) | function useIsMobile() {
FILE: apps/cms/src/hooks/use-plan.ts
type BillingUsage (line 17) | interface BillingUsage {
function usePlan (line 21) | function usePlan() {
FILE: apps/cms/src/hooks/use-readability.ts
type UseReadabilityParams (line 12) | interface UseReadabilityParams {
type ReadabilityLevel (line 17) | type ReadabilityLevel = ReturnType<typeof getReadabilityLevel>;
type ReadabilityResult (line 19) | interface ReadabilityResult {
constant READING_SPEED (line 29) | const READING_SPEED = 238;
function countSyllablesForWord (line 31) | function countSyllablesForWord(word: string): number {
function computeMetrics (line 42) | function computeMetrics(text: string, editor?: Editor | null) {
function useReadability (line 103) | function useReadability({
FILE: apps/cms/src/hooks/use-workspace-id.ts
function useWorkspaceId (line 7) | function useWorkspaceId(): string | null {
FILE: apps/cms/src/lib/actions/checks.ts
function checkCategorySlugAction (line 14) | async function checkCategorySlugAction(
function checkCategorySlugForUpdateAction (line 32) | async function checkCategorySlugForUpdateAction(
function checkTagSlugAction (line 50) | async function checkTagSlugAction(slug: string, workspaceId: string) {
function checkTagSlugForUpdateAction (line 58) | async function checkTagSlugForUpdateAction(
function checkAuthorSlugAction (line 82) | async function checkAuthorSlugAction(slug: string, workspaceId: string) {
function checkAuthorSlugForUpdateAction (line 97) | async function checkAuthorSlugForUpdateAction(
function verifyInvite (line 120) | async function verifyInvite(inviteId: string) {
function checkWorkspaceSlug (line 156) | async function checkWorkspaceSlug(
function checkPostSlugAvailable (line 175) | async function checkPostSlugAvailable(
function checkWorkspaceSubscriptionAction (line 194) | async function checkWorkspaceSubscriptionAction(workspaceId: string) {
function guardWorkspaceSubscriptionAction (line 214) | async function guardWorkspaceSubscriptionAction(
FILE: apps/cms/src/lib/actions/email.ts
type SendInviteEmailProps (line 19) | interface SendInviteEmailProps {
function sendInviteEmailAction (line 28) | async function sendInviteEmailAction({
function sendVerificationEmailAction (line 80) | async function sendVerificationEmailAction({
function sendResetPasswordAction (line 120) | async function sendResetPasswordAction({
function sendWelcomeEmailAction (line 154) | async function sendWelcomeEmailAction({
function sendUsageLimitEmailAction (line 185) | async function sendUsageLimitEmailAction({
function sendFounderEmailAction (line 241) | async function sendFounderEmailAction({
FILE: apps/cms/src/lib/actions/user.ts
function storeUserImageAction (line 10) | async function storeUserImageAction(user: User) {
FILE: apps/cms/src/lib/actions/workspace.ts
function createAuthor (line 15) | async function createAuthor(user: User, organization: Organization) {
function validateWorkspaceSlug (line 68) | async function validateWorkspaceSlug(slug: string | undefined) {
function validateWorkspaceName (line 77) | async function validateWorkspaceName(name: string | undefined) {
function validateWorkspaceTimezone (line 86) | async function validateWorkspaceTimezone(timezone: string | undefined) {
type ValidateWorkspace (line 95) | interface ValidateWorkspace {
function validateWorkspaceSchema (line 101) | async function validateWorkspaceSchema({
FILE: apps/cms/src/lib/ai/readability.ts
type ReadabilityMetrics (line 3) | interface ReadabilityMetrics {
function fetchAiReadabilityRaw (line 11) | async function fetchAiReadabilityRaw(params: {
function parseStringArrayFromText (line 35) | function parseStringArrayFromText(textBody: string): string[] {
function fetchAiReadabilitySuggestionsStrings (line 63) | async function fetchAiReadabilitySuggestionsStrings(params: {
function fetchAiReadabilitySuggestionsObject (line 78) | async function fetchAiReadabilitySuggestionsObject(params: {
FILE: apps/cms/src/lib/auth/access.ts
function requireActiveWorkspaceAccess (line 5) | async function requireActiveWorkspaceAccess() {
FILE: apps/cms/src/lib/auth/client.ts
method onError (line 19) | onError(e) {
FILE: apps/cms/src/lib/auth/redirect.ts
function safeRedirectPath (line 1) | function safeRedirectPath(
FILE: apps/cms/src/lib/auth/server.ts
function getCheckoutReferenceId (line 51) | function getCheckoutReferenceId(body: unknown) {
method sendInvitationEmail (line 214) | async sendInvitationEmail(data) {
method sendVerificationOTP (line 264) | async sendVerificationOTP({ email, otp, type }) {
FILE: apps/cms/src/lib/auth/session.ts
type GetServerSessionOptions (line 4) | interface GetServerSessionOptions {
function getServerSession (line 8) | async function getServerSession(options: GetServerSessionOptions = {}) {
FILE: apps/cms/src/lib/auth/types.ts
type Session (line 4) | type Session = typeof auth.$Infer.Session;
type ActiveOrganization (line 5) | type ActiveOrganization = typeof authClient.$Infer.ActiveOrganization;
type Organization (line 6) | type Organization = typeof authClient.$Infer.Organization;
type Invitation (line 7) | type Invitation = typeof authClient.$Infer.Invitation;
FILE: apps/cms/src/lib/auth/workspace.ts
function setActiveWorkspace (line 14) | async function setActiveWorkspace(slug: string) {
function setClientActiveWorkspace (line 32) | async function setClientActiveWorkspace(slug: string) {
FILE: apps/cms/src/lib/blurhash.ts
constant MAX_CACHE_ENTRIES (line 4) | const MAX_CACHE_ENTRIES = 200;
constant DEFAULT_WIDTH (line 6) | const DEFAULT_WIDTH = 32;
constant DEFAULT_HEIGHT (line 7) | const DEFAULT_HEIGHT = 32;
function blurhashToDataUrl (line 9) | function blurhashToDataUrl(
FILE: apps/cms/src/lib/cache.ts
constant CACHE_PREFIX (line 3) | const CACHE_PREFIX = "cms:cache";
constant DEFAULT_TTL (line 4) | const DEFAULT_TTL = 300;
method get (line 13) | async get<T>(key: string): Promise<T | null> {
method set (line 25) | async set<T>(key: string, value: T, ttl = DEFAULT_TTL): Promise<void> {
method getOrSet (line 36) | async getOrSet<T>(
method invalidatePattern (line 59) | async invalidatePattern(pattern: string): Promise<number> {
method invalidateMedia (line 101) | async invalidateMedia(workspaceId: string): Promise<number> {
method key (line 108) | key(...parts: string[]): string {
FILE: apps/cms/src/lib/cache/invalidate.ts
type CacheResource (line 7) | type CacheResource = "posts" | "categories" | "tags" | "authors" | "usage";
function invalidateCache (line 13) | function invalidateCache(
FILE: apps/cms/src/lib/constants.ts
constant VALID_DISCORD_DOMAINS (line 1) | const VALID_DISCORD_DOMAINS = [
constant VALID_SLACK_DOMAINS (line 7) | const VALID_SLACK_DOMAINS = ["hooks.slack.com"];
constant IMAGE_DROPZONE_ACCEPT (line 11) | const IMAGE_DROPZONE_ACCEPT = [
constant MEDIA_DROPZONE_ACCEPT (line 20) | const MEDIA_DROPZONE_ACCEPT = {
constant ALLOWED_RASTER_MIME_TYPES (line 36) | const ALLOWED_RASTER_MIME_TYPES = [
constant ALLOWED_VIDEO_MIME_TYPES (line 44) | const ALLOWED_VIDEO_MIME_TYPES = [
constant ALLOWED_MIME_TYPES (line 51) | const ALLOWED_MIME_TYPES = [
type AllowedRasterMimeType (line 57) | type AllowedRasterMimeType = (typeof ALLOWED_RASTER_MIME_TYPES)[number];
type AllowedMimeType (line 58) | type AllowedMimeType = (typeof ALLOWED_MIME_TYPES)[number];
constant MAX_AVATAR_FILE_SIZE (line 60) | const MAX_AVATAR_FILE_SIZE = 5 * 1024 * 1024;
constant MAX_LOGO_FILE_SIZE (line 61) | const MAX_LOGO_FILE_SIZE = 5 * 1024 * 1024;
constant MAX_MEDIA_FILE_SIZE (line 62) | const MAX_MEDIA_FILE_SIZE = 250 * 1024 * 1024;
constant WORKSPACE_SCOPED_PREFIXES (line 64) | const WORKSPACE_SCOPED_PREFIXES = [
type WorkspaceScopedPrefix (line 79) | type WorkspaceScopedPrefix = (typeof WORKSPACE_SCOPED_PREFIXES)[number];
constant ALLOWED_AVATAR_HOSTS (line 81) | const ALLOWED_AVATAR_HOSTS = [
function isAllowedAvatarUrl (line 89) | function isAllowedAvatarUrl(url: string): boolean {
constant SOCIAL_PLATFORMS (line 111) | const SOCIAL_PLATFORMS = {
type SocialPlatform (line 125) | type SocialPlatform = keyof typeof SOCIAL_PLATFORMS;
constant PLATFORM_DOMAINS (line 127) | const PLATFORM_DOMAINS = {
constant MEDIA_SORT_BY (line 140) | const MEDIA_SORT_BY = ["createdAt", "name"] as const;
constant SORT_DIRECTIONS (line 141) | const SORT_DIRECTIONS = ["asc", "desc"] as const;
constant MEDIA_SORTS (line 143) | const MEDIA_SORTS = MEDIA_SORT_BY.flatMap((field) =>
constant MEDIA_TYPES (line 147) | const MEDIA_TYPES = ["image", "video", "audio", "document"] as const;
constant MEDIA_FILTER_TYPES (line 149) | const MEDIA_FILTER_TYPES = ["all", ...MEDIA_TYPES] as const;
constant MEDIA_LIMIT (line 151) | const MEDIA_LIMIT = 20;
constant POST_LIMIT (line 152) | const POST_LIMIT = 20;
constant RESERVED_WORKSPACE_SLUGS (line 158) | const RESERVED_WORKSPACE_SLUGS = [
FILE: apps/cms/src/lib/custom-fields.ts
type CustomFieldPayload (line 9) | type CustomFieldPayload = z.infer<typeof customFieldsPayloadSchema>;
type CustomFieldValidationDefinition (line 11) | interface CustomFieldValidationDefinition {
constant SUPPORTED_CUSTOM_FIELD_TYPES (line 23) | const SUPPORTED_CUSTOM_FIELD_TYPES = new Set<FieldType>([
function normalizeMultiselectValue (line 33) | function normalizeMultiselectValue(
function isRichTextContentEmpty (line 79) | function isRichTextContentEmpty(content: string) {
function isMultiselectValueEmpty (line 92) | function isMultiselectValueEmpty(rawValue: string) {
function normalizeCustomFieldValue (line 123) | function normalizeCustomFieldValue(
function validateCustomFieldValue (line 173) | function validateCustomFieldValue(
function resolveCustomFieldValues (line 206) | function resolveCustomFieldValues(
FILE: apps/cms/src/lib/data/post.ts
function todayUTCMidnight (line 3) | function todayUTCMidnight() {
FILE: apps/cms/src/lib/events/dispatch.ts
type EmitDashboardEventArgs (line 8) | interface EmitDashboardEventArgs {
function logDashboardEventError (line 18) | function logDashboardEventError(error: unknown) {
function emitDashboardEvent (line 22) | async function emitDashboardEvent({
FILE: apps/cms/src/lib/media/upload.ts
type UploadMetadata (line 6) | interface UploadMetadata {
constant BLURHASH_RASTER_TYPES (line 14) | const BLURHASH_RASTER_TYPES = new Set([
function getPresignedUrl (line 25) | async function getPresignedUrl(
function uploadToR2 (line 45) | async function uploadToR2(presignedUrl: string, file: File) {
function completeUpload (line 60) | async function completeUpload(
function loadImage (line 103) | function loadImage(file: File): Promise<HTMLImageElement> {
function encodeBlurHash (line 123) | function encodeBlurHash(image: HTMLImageElement) {
function getImageMetadata (line 148) | async function getImageMetadata(file: File): Promise<UploadMetadata> {
function getVideoMetadata (line 166) | function getVideoMetadata(file: File): Promise<UploadMetadata> {
function getUploadMetadata (line 195) | async function getUploadMetadata(file: File): Promise<UploadMetadata> {
function uploadFile (line 214) | async function uploadFile({
FILE: apps/cms/src/lib/notifications.ts
type NotificationPreferences (line 1) | interface NotificationPreferences {
type NotificationToggleItem (line 12) | interface NotificationToggleItem {
constant DEFAULT_NOTIFICATION_PREFERENCES (line 19) | const DEFAULT_NOTIFICATION_PREFERENCES: NotificationPreferences = {
constant USER_NOTIFICATION_ITEMS (line 30) | const USER_NOTIFICATION_ITEMS: NotificationToggleItem[] = [
constant WORKSPACE_NOTIFICATION_ITEMS (line 47) | const WORKSPACE_NOTIFICATION_ITEMS: NotificationToggleItem[] = [
FILE: apps/cms/src/lib/polar/client.ts
function createPolarClient (line 5) | function createPolarClient(): Polar | null {
FILE: apps/cms/src/lib/polar/customer.created.ts
function handleCustomerCreated (line 5) | async function handleCustomerCreated(
FILE: apps/cms/src/lib/polar/subscription.canceled.ts
function handleSubscriptionCanceled (line 8) | async function handleSubscriptionCanceled(
FILE: apps/cms/src/lib/polar/subscription.created.ts
function handleSubscriptionCreated (line 11) | async function handleSubscriptionCreated(
FILE: apps/cms/src/lib/polar/subscription.revoked.ts
function handleSubscriptionRevoked (line 8) | async function handleSubscriptionRevoked(
FILE: apps/cms/src/lib/polar/subscription.updated.ts
function handleSubscriptionUpdated (line 12) | async function handleSubscriptionUpdated(
FILE: apps/cms/src/lib/polar/utils.ts
function isStalePolarEvent (line 7) | function isStalePolarEvent(
function getPlanType (line 14) | function getPlanType(productName: string): PlanType | null {
function getSubscriptionStatus (line 25) | function getSubscriptionStatus(
function getRecurringInterval (line 46) | function getRecurringInterval(
FILE: apps/cms/src/lib/queries/keys.ts
constant QUERY_KEYS (line 1) | const QUERY_KEYS = {
FILE: apps/cms/src/lib/queries/user.ts
function getInitialUserData (line 4) | async function getInitialUserData() {
FILE: apps/cms/src/lib/queries/workspace.ts
function getLastActiveWorkspaceOrNewOneToSetAsActive (line 24) | async function getLastActiveWorkspaceOrNewOneToSetAsActive(
function getInitialWorkspaceData (line 102) | async function getInitialWorkspaceData(
function validateWorkspaceAccess (line 211) | async function validateWorkspaceAccess(slug: string): Promise<boolean> {
FILE: apps/cms/src/lib/r2.ts
constant ACCESS_KEY_ID (line 4) | const ACCESS_KEY_ID = process.env.CLOUDFLARE_ACCESS_KEY_ID;
constant SECRET_ACCESS_KEY (line 5) | const SECRET_ACCESS_KEY = process.env.CLOUDFLARE_SECRET_ACCESS_KEY;
constant BUCKET_NAME (line 6) | const BUCKET_NAME = process.env.CLOUDFLARE_BUCKET_NAME;
constant ENDPOINT (line 7) | const ENDPOINT = process.env.CLOUDFLARE_S3_ENDPOINT;
constant PUBLIC_URL (line 8) | const PUBLIC_URL = process.env.CLOUDFLARE_PUBLIC_URL;
constant R2_BUCKET_NAME (line 29) | const R2_BUCKET_NAME = BUCKET_NAME;
constant R2_PUBLIC_URL (line 30) | const R2_PUBLIC_URL = PUBLIC_URL;
FILE: apps/cms/src/lib/search-params.ts
function parseAsSort (line 20) | function parseAsSort<const Field extends string>(fields: readonly Field[...
constant POST_SORT_BY (line 83) | const POST_SORT_BY = [
constant POST_SORTS (line 90) | const POST_SORTS = POST_SORT_BY.flatMap((field) =>
FILE: apps/cms/src/lib/validations/auth.ts
type CredentialData (line 15) | type CredentialData = z.infer<typeof credentialSchema>;
type InviteData (line 21) | type InviteData = z.infer<typeof inviteSchema>;
FILE: apps/cms/src/lib/validations/authors.ts
type CreateAuthorValues (line 71) | type CreateAuthorValues = z.infer<typeof authorSchema>;
type SocialLink (line 72) | type SocialLink = z.infer<typeof socialLinkSchema>;
FILE: apps/cms/src/lib/validations/fields.ts
function validateUniqueOptionValues (line 30) | function validateUniqueOptionValues(
function validateFieldOptions (line 50) | function validateFieldOptions(
type CustomFieldFormValues (line 102) | type CustomFieldFormValues = z.infer<typeof customFieldSchema>;
type CustomFieldUpdateValues (line 134) | type CustomFieldUpdateValues = z.infer<typeof customFieldUpdateSchema>;
type FieldType (line 136) | type FieldType = z.infer<typeof fieldTypeEnum>;
type FieldOptionInput (line 137) | type FieldOptionInput = z.infer<typeof fieldOptionSchema>;
FILE: apps/cms/src/lib/validations/keys.ts
type CreateApiKeyValues (line 20) | type CreateApiKeyValues = z.infer<typeof createApiKeySchema>;
type UpdateApiKeyValues (line 33) | type UpdateApiKeyValues = z.infer<typeof updateApiKeySchema>;
type ApiKeyValues (line 35) | type ApiKeyValues = CreateApiKeyValues | UpdateApiKeyValues;
FILE: apps/cms/src/lib/validations/post.ts
type PostValues (line 33) | type PostValues = z.infer<typeof postSchema>;
type PostEditorValues (line 39) | type PostEditorValues = z.infer<typeof postEditorSchema>;
type PostUpsertValues (line 47) | type PostUpsertValues = z.infer<typeof postUpsertSchema>;
type ShareLinkValues (line 53) | type ShareLinkValues = z.infer<typeof shareLinkSchema>;
type PostImportValues (line 73) | type PostImportValues = z.infer<typeof postImportSchema>;
FILE: apps/cms/src/lib/validations/settings.ts
type ProfileData (line 7) | type ProfileData = z.infer<typeof profileSchema>;
type BillingData (line 17) | type BillingData = z.infer<typeof billingSchema>;
FILE: apps/cms/src/lib/validations/tags.ts
function validateWorkspaceTags (line 10) | async function validateWorkspaceTags(
FILE: apps/cms/src/lib/validations/upload.ts
function validateUpload (line 129) | function validateUpload({
FILE: apps/cms/src/lib/validations/webhook.ts
type WebhookFormValues (line 80) | type WebhookFormValues = z.infer<typeof webhookSchema>;
type WebhookUpdateValues (line 147) | type WebhookUpdateValues = z.infer<typeof webhookUpdateSchema>;
type WebhookEvent (line 149) | type WebhookEvent = z.infer<typeof webhookEventEnum>;
type PayloadFormat (line 150) | type PayloadFormat = z.infer<typeof payloadFormatEnum>;
FILE: apps/cms/src/lib/validations/workspace.ts
type CreateTagValues (line 25) | type CreateTagValues = z.infer<typeof tagSchema>;
type CreateCategoryValues (line 33) | type CreateCategoryValues = z.infer<typeof categorySchema>;
type CreateWorkspaceValues (line 48) | type CreateWorkspaceValues = z.infer<typeof workspaceSchema>;
type NameValues (line 58) | type NameValues = z.infer<typeof nameSchema>;
type SlugValues (line 64) | type SlugValues = z.infer<typeof slugSchema>;
type TimezoneValues (line 70) | type TimezoneValues = z.infer<typeof timezoneSchema>;
FILE: apps/cms/src/providers/user.tsx
type UserProviderProps (line 13) | interface UserProviderProps {
function UserProvider (line 20) | function UserProvider({ children, initialUser }: UserProviderProps) {
FILE: apps/cms/src/providers/workspace.tsx
function WorkspaceProvider (line 32) | function WorkspaceProvider({
function useWorkspace (line 154) | function useWorkspace() {
FILE: apps/cms/src/proxy.ts
function proxy (line 7) | async function proxy(request: NextRequest) {
FILE: apps/cms/src/types/author.ts
type AuthorSocial (line 3) | interface AuthorSocial {
type Author (line 9) | interface Author {
FILE: apps/cms/src/types/dashboard.ts
type DashboardRecentUpload (line 3) | type DashboardRecentUpload = Pick<
type UsageDashboardData (line 21) | interface UsageDashboardData {
type PublishingMetricsData (line 55) | interface PublishingMetricsData {
FILE: apps/cms/src/types/fields.ts
type FieldOption (line 1) | interface FieldOption {
type CustomField (line 12) | interface CustomField {
FILE: apps/cms/src/types/icons.ts
type SVGAttributes (line 3) | type SVGAttributes = Partial<SVGProps<SVGSVGElement>>;
type ElementAttributes (line 4) | type ElementAttributes = RefAttributes<SVGSVGElement> & SVGAttributes;
type IconProps (line 6) | interface IconProps extends ElementAttributes {
FILE: apps/cms/src/types/media.ts
type MediaType (line 9) | type MediaType = (typeof MEDIA_TYPES)[number];
type MediaFilterType (line 11) | type MediaFilterType = (typeof MEDIA_FILTER_TYPES)[number];
type UploadType (line 13) | type UploadType = "avatar" | "logo" | "media";
type Media (line 15) | interface Media {
type MediaSort (line 30) | type MediaSort = (typeof MEDIA_SORTS)[number];
type MediaQueryKey (line 32) | type MediaQueryKey = [
type MediaPaginatedListResponse (line 43) | interface MediaPaginatedListResponse {
type MediaCursorListResponse (line 50) | interface MediaCursorListResponse {
type MediaListResponse (line 56) | type MediaListResponse =
type PresignedUrlResponse (line 61) | interface PresignedUrlResponse {
type UploadResponse (line 67) | interface UploadResponse {
type UploadResponseMap (line 76) | interface UploadResponseMap {
FILE: apps/cms/src/types/misc.ts
type AuthMethod (line 1) | type AuthMethod = "google" | "github" | "email";
FILE: apps/cms/src/types/share.ts
type ShareAuthor (line 3) | interface ShareAuthor {
type ShareCategory (line 10) | interface ShareCategory {
type ShareTag (line 16) | interface ShareTag {
type ShareWorkspace (line 22) | interface ShareWorkspace {
type SharePost (line 29) | interface SharePost {
type ShareData (line 46) | interface ShareData {
type SharePageClientProps (line 51) | interface SharePageClientProps {
type ShareStatus (line 57) | type ShareStatus = "expired" | "not-found";
type ShareLinkResponse (line 60) | interface ShareLinkResponse {
type ShareErrorResponse (line 65) | interface ShareErrorResponse {
FILE: apps/cms/src/types/user.ts
type UserProfile (line 3) | interface UserProfile extends Omit<User, "emailVerified"> {
type UserContextType (line 20) | interface UserContextType {
FILE: apps/cms/src/types/webhook.ts
type Webhook (line 1) | interface Webhook {
FILE: apps/cms/src/types/workspace.ts
type Workspace (line 3) | interface Workspace {
type WorkspaceContextType (line 45) | interface WorkspaceContextType {
type WorkspaceProviderProps (line 57) | interface WorkspaceProviderProps {
FILE: apps/cms/src/utils/author.tsx
function detectPlatform (line 18) | function detectPlatform(url: string): SocialPlatform {
function getPlatformDisplayName (line 41) | function getPlatformDisplayName(platform: SocialPlatform): string {
FILE: apps/cms/src/utils/fetch/client.ts
type RequestMethod (line 3) | type RequestMethod = "GET" | "POST" | "PUT" | "DELETE" | "PATCH";
function request (line 5) | async function request<T>(
FILE: apps/cms/src/utils/keys.ts
type ApiKeyPrefix (line 6) | type ApiKeyPrefix = (typeof PREFIXES)[keyof typeof PREFIXES];
constant DEFAULT_PUBLIC_SCOPES (line 11) | const DEFAULT_PUBLIC_SCOPES = [
constant DEFAULT_PRIVATE_SCOPES (line 22) | const DEFAULT_PRIVATE_SCOPES = [
function getApiKeyType (line 40) | function getApiKeyType(key: string): "public" | "private" | null {
constant VALID_SCOPES (line 53) | const VALID_SCOPES = [
type ApiScope (line 66) | type ApiScope = (typeof VALID_SCOPES)[number];
function parseScopes (line 73) | function parseScopes(permissions: string | null): ApiScope[] {
function hasScope (line 92) | function hasScope(scopes: ApiScope[], scope: ApiScope): boolean {
function validateScopes (line 101) | function validateScopes(scopes: string[]): boolean {
FILE: apps/cms/src/utils/media.ts
function getMediaType (line 9) | function getMediaType(mimeType: string): MediaType {
function getEmptyStateMessage (line 22) | function getEmptyStateMessage(type?: MediaType, hasAnyMedia?: boolean) {
function isMediaSort (line 40) | function isMediaSort(value: string): value is MediaSort {
function splitMediaSort (line 47) | function splitMediaSort(sort: MediaSort) {
function isMediaFilterType (line 55) | function isMediaFilterType(
function toMediaType (line 63) | function toMediaType(value: MediaFilterType): MediaType | undefined {
function downloadMedia (line 67) | async function downloadMedia(media: Media) {
function formatMediaType (line 87) | function formatMediaType(media: Media) {
function formatMediaDimensions (line 91) | function formatMediaDimensions(media: Media) {
function formatMediaDuration (line 98) | function formatMediaDuration(duration: number | null | undefined) {
FILE: apps/cms/src/utils/readability.ts
function calculateReadabilityScore (line 3) | function calculateReadabilityScore(editor: Editor): number {
function countSyllables (line 35) | function countSyllables(word: string): number {
function getReadabilityLevel (line 51) | function getReadabilityLevel(score: number): {
function generateSuggestions (line 97) | function generateSuggestions(metrics: {
FILE: apps/cms/src/utils/site.ts
constant SITE_CONFIG (line 1) | const SITE_CONFIG = {
FILE: apps/cms/src/utils/string.ts
function formatCalendarDate (line 8) | function formatCalendarDate(date: Date, formatStr: string) {
function generateSlug (line 17) | function generateSlug(
function formatBytes (line 31) | function formatBytes(
FILE: apps/cms/src/utils/usage/media.ts
function getCustomerIdForWorkspace (line 8) | async function getCustomerIdForWorkspace(
function trackMediaUploadInDB (line 38) | async function trackMediaUploadInDB(
function trackMediaUploadInPolar (line 54) | async function trackMediaUploadInPolar(
function trackMediaUpload (line 82) | async function trackMediaUpload(
FILE: apps/jobs/src/consumers/deliveries.ts
constant WEBHOOK_DELIVERY_TIMEOUT_MS (line 12) | const WEBHOOK_DELIVERY_TIMEOUT_MS = 15_000;
type DeliveryMessage (line 14) | interface DeliveryMessage {
function handleWebhookDeliveryQueue (line 18) | async function handleWebhookDeliveryQueue(
function processDelivery (line 40) | async function processDelivery(
FILE: apps/jobs/src/consumers/dlq.ts
type DlqMessage (line 4) | interface DlqMessage {
function handleDeadLetterQueue (line 9) | async function handleDeadLetterQueue(
FILE: apps/jobs/src/consumers/events.ts
type EventMessage (line 4) | interface EventMessage {
function handleEventQueue (line 10) | async function handleEventQueue(
FILE: apps/jobs/src/index.ts
method fetch (line 8) | async fetch() {
method queue (line 12) | async queue(batch: MessageBatch, env: Env, _ctx: ExecutionContext) {
method scheduled (line 35) | async scheduled(event: ScheduledEvent, env: Env, ctx: ExecutionContext) {
FILE: apps/jobs/src/lib/db.ts
type DbClient (line 4) | type DbClient = ReturnType<typeof createDbClient>;
function createDbClient (line 6) | function createDbClient(env: Env) {
FILE: apps/jobs/src/lib/formats.ts
type WebhookPayload (line 3) | type WebhookPayload = ReturnType<typeof buildWebhookPayload>;
type PayloadFormat (line 5) | type PayloadFormat = "json" | "discord" | "slack";
type WebhookData (line 7) | type WebhookData = Record<string, unknown>;
type DiscordEmbedField (line 9) | interface DiscordEmbedField {
type DiscordWebhookBody (line 15) | interface DiscordWebhookBody {
type SlackWebhookBody (line 37) | interface SlackWebhookBody {
constant MARBLE_COLOR (line 42) | const MARBLE_COLOR = 5_786_879;
constant MARBLE_AVATAR_URL (line 43) | const MARBLE_AVATAR_URL = "https://marblecms.com/logo.svg";
function formatEvent (line 45) | function formatEvent(input: string) {
function getData (line 52) | function getData(payload: WebhookPayload): WebhookData {
function stringify (line 60) | function stringify(value: unknown) {
function getDisplayFields (line 73) | function getDisplayFields(payload: WebhookPayload) {
function buildDiscordWebhookBody (line 104) | function buildDiscordWebhookBody(
function buildSlackWebhookBody (line 137) | function buildSlackWebhookBody(
function buildWebhookRequestBody (line 186) | function buildWebhookRequestBody(
FILE: apps/jobs/src/lib/signing.ts
function signPayload (line 3) | async function signPayload(
FILE: apps/jobs/src/lib/usage.ts
type UsageThreshold (line 6) | type UsageThreshold = 75 | 90 | 100;
type UsageAlertKind (line 7) | type UsageAlertKind = "warning" | "critical" | "exhausted";
constant USAGE_ALERT_THRESHOLDS (line 9) | const USAGE_ALERT_THRESHOLDS = {
type UsagePeriod (line 15) | interface UsagePeriod {
type WebhookUsageCheck (line 20) | interface WebhookUsageCheck {
function isUniqueConstraintError (line 32) | function isUniqueConstraintError(error: unknown) {
function getValidDate (line 45) | function getValidDate(year: number, month: number, day: number) {
function getBillingPeriod (line 57) | async function getBillingPeriod(
function getCrossedAlertKind (line 125) | function getCrossedAlertKind(
function checkWebhookUsage (line 163) | async function checkWebhookUsage(
function recordWebhookUsage (line 208) | async function recordWebhookUsage(
function sendWebhookUsageAlert (line 228) | async function sendWebhookUsageAlert(
FILE: apps/jobs/src/scheduled/cleanup.ts
function handleCleanup (line 3) | async function handleCleanup(
FILE: apps/jobs/src/types/env.ts
type Env (line 1) | interface Env {
FILE: apps/mcp/public/home.js
function initCopyButtons (line 1) | function initCopyButtons() {
FILE: apps/mcp/src/components/mcp-clients.tsx
type McpClient (line 9) | interface McpClient {
constant MCP_CLIENTS (line 15) | const MCP_CLIENTS: McpClient[] = [
FILE: apps/mcp/src/lib/api.ts
type ApiBody (line 4) | type ApiBody = Record<string, unknown> | null;
function buildUrl (line 10) | function buildUrl(apiBaseUrl: string, path: string, query?: QueryParams) {
function readJsonApi (line 32) | async function readJsonApi(
function validateApiKey (line 41) | async function validateApiKey(apiBaseUrl: string, apiKey: string) {
function writeJsonApi (line 49) | async function writeJsonApi(
function uploadMediaApi (line 64) | async function uploadMediaApi(
function deleteJsonApi (line 113) | async function deleteJsonApi(
function requestJsonApi (line 121) | async function requestJsonApi(
function parseApiBody (line 168) | function parseApiBody(text: string): ApiBody {
FILE: apps/mcp/src/lib/auth.ts
function getApiKey (line 6) | function getApiKey(request: Request) {
function authHeaderValue (line 28) | function authHeaderValue(apiKey: string) {
function parseAuthorizationHeader (line 43) | function parseAuthorizationHeader(header: string | null) {
FILE: apps/mcp/src/lib/constants.ts
constant DEFAULT_API_BASE_URL (line 1) | const DEFAULT_API_BASE_URL = "https://api.marblecms.com";
constant MCP_TOOL_GROUPS (line 3) | const MCP_TOOL_GROUPS = [
FILE: apps/mcp/src/lib/instructions.ts
function getServerInstructions (line 4) | function getServerInstructions() {
FILE: apps/mcp/src/lib/mcp.ts
function toolResult (line 6) | function toolResult(data: Record<string, unknown>) {
FILE: apps/mcp/src/lib/media.ts
constant MAX_REMOTE_MEDIA_BYTES (line 1) | const MAX_REMOTE_MEDIA_BYTES = 5 * 1024 * 1024;
constant REMOTE_MEDIA_FETCH_TIMEOUT_MS (line 2) | const REMOTE_MEDIA_FETCH_TIMEOUT_MS = 10_000;
function filenameFromUrl (line 4) | function filenameFromUrl(url: string) {
function assertPrivateApiKey (line 14) | function assertPrivateApiKey(apiKey: string) {
function fetchRemoteMedia (line 22) | async function fetchRemoteMedia(url: string) {
function rawApiKey (line 70) | function rawApiKey(apiKey: string) {
function assertAllowedRemoteUrl (line 74) | function assertAllowedRemoteUrl(url: URL) {
FILE: apps/mcp/src/server.ts
function createServer (line 9) | function createServer(apiBaseUrl: string, apiKey: string) {
FILE: apps/mcp/src/tools/authors.ts
function registerAuthorTools (line 72) | function registerAuthorTools(
FILE: apps/mcp/src/tools/categories.ts
function registerCategoryTools (line 14) | function registerCategoryTools(
FILE: apps/mcp/src/tools/media.ts
function registerMediaTools (line 37) | function registerMediaTools(
FILE: apps/mcp/src/tools/posts.ts
function registerPostTools (line 126) | function registerPostTools(
FILE: apps/mcp/src/tools/tags.ts
function registerTagTools (line 14) | function registerTagTools(
FILE: apps/mcp/src/types.ts
type Env (line 1) | interface Env {
type QueryParams (line 5) | type QueryParams = Record<
FILE: apps/web/src/lib/constants/faqs.ts
constant PRICING_FAQS (line 67) | const PRICING_FAQS: {
FILE: apps/web/src/lib/constants/landing.ts
constant FEATURES (line 12) | const FEATURES = [
constant USERS (line 63) | const USERS = [
constant REVIEWS (line 102) | const REVIEWS = [
FILE: apps/web/src/lib/constants/navigation.ts
type Link (line 7) | interface Link {
constant SOCIAL_LINKS (line 12) | const SOCIAL_LINKS: Link[] = [
type FooterLink (line 20) | interface FooterLink {
type FooterSection (line 29) | interface FooterSection {
constant FOOTER_SECTIONS (line 34) | const FOOTER_SECTIONS: FooterSection[] = [
constant FOOTER_SOCIAL_LINKS (line 139) | const FOOTER_SOCIAL_LINKS: FooterLink[] = [
FILE: apps/web/src/lib/constants/site.ts
type Site (line 1) | interface Site {
constant SITE (line 11) | const SITE: Site = {
FILE: apps/web/src/lib/constants/tracking.ts
constant REGISTER_URL (line 3) | const REGISTER_URL = `${SITE.APP_URL}/register`;
constant TRACKING_EVENTS (line 5) | const TRACKING_EVENTS = {
FILE: apps/web/src/lib/schemas.ts
type Post (line 57) | type Post = z.infer<typeof postSchema>;
type Posts (line 63) | type Posts = z.infer<typeof postsSchema>;
type Category (line 74) | type Category = z.infer<typeof categorySchema>;
FILE: apps/web/src/lib/seo.ts
constant DESCRIPTION_MAX_LENGTH (line 1) | const DESCRIPTION_MAX_LENGTH = 160;
function cleanMetaDescription (line 3) | function cleanMetaDescription(
function jsonLd (line 16) | function jsonLd(schema: unknown) {
function stripHtml (line 20) | function stripHtml(html: string) {
function buildSiteJsonLd (line 27) | function buildSiteJsonLd(site: {
function buildFaqJsonLd (line 56) | function buildFaqJsonLd(
function buildArticleJsonLd (line 73) | function buildArticleJsonLd({
FILE: apps/web/src/lib/site.ts
constant HERO_VARIATIONS (line 1) | const HERO_VARIATIONS = {
constant HERO (line 19) | const HERO = HERO_VARIATIONS.simple;
FILE: apps/web/src/lib/utils.ts
function calculateReadTime (line 4) | function calculateReadTime(content: string) {
function cn (line 13) | function cn(...inputs: ClassValue[]) {
FILE: apps/web/src/pages/rss.xml.ts
function GET (line 6) | async function GET(context: APIContext) {
FILE: packages/db/prisma/migrations/0_init/migration.sql
type "public" (line 23) | CREATE TABLE "public"."subscription" (
type "public" (line 43) | CREATE TABLE "public"."workspace" (
type "public" (line 59) | CREATE TABLE "public"."post" (
type "public" (line 82) | CREATE TABLE "public"."tag" (
type "public" (line 95) | CREATE TABLE "public"."media" (
type "public" (line 109) | CREATE TABLE "public"."category" (
type "public" (line 122) | CREATE TABLE "public"."webhook" (
type "public" (line 138) | CREATE TABLE "public"."user" (
type "public" (line 151) | CREATE TABLE "public"."session" (
type "public" (line 166) | CREATE TABLE "public"."account" (
type "public" (line 185) | CREATE TABLE "public"."verification" (
type "public" (line 197) | CREATE TABLE "public"."member" (
type "public" (line 208) | CREATE TABLE "public"."invitation" (
type "public" (line 221) | CREATE TABLE "public"."_PostToTag" (
type "public" (line 229) | CREATE TABLE "public"."_PostToUser" (
type "public" (line 237) | CREATE UNIQUE INDEX "subscription_polarId_key" ON "public"."subscription...
type "public" (line 240) | CREATE UNIQUE INDEX "subscription_workspaceId_key" ON "public"."subscrip...
type "public" (line 243) | CREATE UNIQUE INDEX "workspace_slug_key" ON "public"."workspace"("slug")
type "public" (line 246) | CREATE UNIQUE INDEX "workspace_subdomain_key" ON "public"."workspace"("s...
type "public" (line 249) | CREATE UNIQUE INDEX "post_workspaceId_slug_key" ON "public"."post"("work...
type "public" (line 252) | CREATE UNIQUE INDEX "tag_workspaceId_slug_key" ON "public"."tag"("worksp...
type "public" (line 255) | CREATE UNIQUE INDEX "category_workspaceId_slug_key" ON "public"."categor...
type "public" (line 258) | CREATE UNIQUE INDEX "user_email_key" ON "public"."user"("email")
type "public" (line 261) | CREATE UNIQUE INDEX "session_token_key" ON "public"."session"("token")
type "public" (line 264) | CREATE INDEX "_PostToTag_B_index" ON "public"."_PostToTag"("B")
type "public" (line 267) | CREATE INDEX "_PostToUser_B_index" ON "public"."_PostToUser"("B")
FILE: packages/db/prisma/migrations/20250831193214_add_author_table/migration.sql
type "public" (line 5) | CREATE TABLE "public"."author" (
type "public" (line 24) | CREATE TABLE "public"."_PostToAuthor" (
type "public" (line 32) | CREATE UNIQUE INDEX "author_workspaceId_userId_key" ON "public"."author"...
type "public" (line 35) | CREATE UNIQUE INDEX "author_workspaceId_slug_key" ON "public"."author"("...
type "public" (line 38) | CREATE INDEX "_PostToAuthor_B_index" ON "public"."_PostToAuthor"("B")
FILE: packages/db/prisma/migrations/20250915114755_add_database_indices/migration.sql
type "public" (line 2) | CREATE INDEX "account_userId_idx" ON "public"."account"("userId")
type "public" (line 5) | CREATE INDEX "account_providerId_accountId_idx" ON "public"."account"("p...
type "public" (line 8) | CREATE INDEX "author_workspaceId_isActive_idx" ON "public"."author"("wor...
type "public" (line 11) | CREATE INDEX "author_userId_idx" ON "public"."author"("userId")
type "public" (line 14) | CREATE INDEX "category_workspaceId_idx" ON "public"."category"("workspac...
type "public" (line 17) | CREATE INDEX "invitation_organizationId_idx" ON "public"."invitation"("o...
type "public" (line 20) | CREATE INDEX "invitation_email_idx" ON "public"."invitation"("email")
type "public" (line 23) | CREATE INDEX "invitation_inviterId_idx" ON "public"."invitation"("invite...
type "public" (line 26) | CREATE INDEX "media_workspaceId_createdAt_idx" ON "public"."media"("work...
type "public" (line 29) | CREATE INDEX "media_workspaceId_type_idx" ON "public"."media"("workspace...
type "public" (line 32) | CREATE INDEX "member_userId_idx" ON "public"."member"("userId")
type "public" (line 35) | CREATE INDEX "member_organizationId_idx" ON "public"."member"("organizat...
type "public" (line 38) | CREATE INDEX "member_organizationId_userId_idx" ON "public"."member"("or...
type "public" (line 41) | CREATE INDEX "post_workspaceId_status_idx" ON "public"."post"("workspace...
type "public" (line 44) | CREATE INDEX "post_workspaceId_createdAt_idx" ON "public"."post"("worksp...
type "public" (line 47) | CREATE INDEX "post_workspaceId_status_publishedAt_idx" ON "public"."post...
type "public" (line 50) | CREATE INDEX "post_categoryId_idx" ON "public"."post"("categoryId")
type "public" (line 53) | CREATE INDEX "session_userId_idx" ON "public"."session"("userId")
type "public" (line 56) | CREATE INDEX "session_activeOrganizationId_idx" ON "public"."session"("a...
type "public" (line 59) | CREATE INDEX "subscription_userId_idx" ON "public"."subscription"("userId")
type "public" (line 62) | CREATE INDEX "subscription_status_idx" ON "public"."subscription"("status")
type "public" (line 65) | CREATE INDEX "tag_workspaceId_idx" ON "public"."tag"("workspaceId")
type "public" (line 68) | CREATE INDEX "webhook_workspaceId_idx" ON "public"."webhook"("workspaceId")
type "public" (line 71) | CREATE INDEX "webhook_workspaceId_enabled_idx" ON "public"."webhook"("wo...
FILE: packages/db/prisma/migrations/20250919210238_add_share_link_table/migration.sql
type "public" (line 2) | CREATE TABLE "public"."ShareLink" (
type "public" (line 17) | CREATE UNIQUE INDEX "ShareLink_token_key" ON "public"."ShareLink"("token")
type "public" (line 20) | CREATE INDEX "ShareLink_postId_idx" ON "public"."ShareLink"("postId")
type "public" (line 23) | CREATE INDEX "ShareLink_workspaceId_idx" ON "public"."ShareLink"("worksp...
type "public" (line 26) | CREATE INDEX "ShareLink_expiresAt_idx" ON "public"."ShareLink"("expiresAt")
type "public" (line 29) | CREATE INDEX "ShareLink_isActive_idx" ON "public"."ShareLink"("isActive")
FILE: packages/db/prisma/migrations/20250923212858_add_ai_editor_preferences/migration.sql
type "public" (line 2) | CREATE TABLE "public"."editor_preferences" (
type "public" (line 10) | CREATE TABLE "public"."ai" (
type "public" (line 19) | CREATE UNIQUE INDEX "editor_preferences_workspaceId_key" ON "public"."ed...
type "public" (line 22) | CREATE UNIQUE INDEX "ai_editorPreferencesId_key" ON "public"."ai"("edito...
FILE: packages/db/prisma/migrations/20250924180405_add_missing_better_auth_indices/migration.sql
type "public" (line 2) | CREATE INDEX "session_token_idx" ON "public"."session"("token")
type "public" (line 5) | CREATE INDEX "verification_identifier_idx" ON "public"."verification"("i...
FILE: packages/db/prisma/migrations/20250927161627_add_author_social_links/migration.sql
type "public" (line 11) | CREATE TABLE "public"."author_social" (
type "public" (line 23) | CREATE INDEX "author_social_authorId_idx" ON "public"."author_social"("a...
FILE: packages/db/prisma/migrations/20251114225009_add_usage_event_table/migration.sql
type "usage_event" (line 5) | CREATE TABLE "usage_event" (
type "usage_event" (line 17) | CREATE INDEX "usage_event_workspaceId_type_createdAt_idx" ON "usage_even...
type "usage_event" (line 20) | CREATE INDEX "usage_event_workspaceId_createdAt_idx" ON "usage_event"("w...
FILE: packages/db/prisma/migrations/20251201001521_add_api_keys/migration.sql
type "public" (line 8) | CREATE TABLE "public"."api_key" (
type "public" (line 32) | CREATE UNIQUE INDEX "api_key_key_key" ON "public"."api_key"("key")
type "public" (line 35) | CREATE INDEX "api_key_workspaceId_idx" ON "public"."api_key"("workspaceId")
type "public" (line 38) | CREATE INDEX "api_key_workspaceId_createdAt_idx" ON "public"."api_key"("...
type "public" (line 41) | CREATE INDEX "api_key_workspaceId_enabled_idx" ON "public"."api_key"("wo...
type "public" (line 44) | CREATE INDEX "api_key_workspaceId_type_idx" ON "public"."api_key"("works...
type "public" (line 47) | CREATE INDEX "api_key_key_idx" ON "public"."api_key"("key")
FILE: packages/db/prisma/migrations/20251210213108_subscription_history/migration.sql
type "public" (line 47) | CREATE INDEX "subscription_workspaceId_status_idx" ON "public"."subscrip...
FILE: packages/db/prisma/migrations/20260331143009_add_fields/migration.sql
type "public" (line 11) | CREATE TABLE "public"."field" (
type "public" (line 27) | CREATE TABLE "public"."field_option" (
type "public" (line 41) | CREATE TABLE "public"."field_value" (
type "public" (line 54) | CREATE INDEX "field_workspaceId_idx" ON "public"."field"("workspaceId")
type "public" (line 57) | CREATE UNIQUE INDEX "field_workspaceId_key_key" ON "public"."field"("wor...
type "public" (line 60) | CREATE UNIQUE INDEX "field_id_workspaceId_key" ON "public"."field"("id",...
type "public" (line 63) | CREATE INDEX "field_option_fieldId_idx" ON "public"."field_option"("fiel...
type "public" (line 66) | CREATE INDEX "field_option_workspaceId_idx" ON "public"."field_option"("...
type "public" (line 69) | CREATE INDEX "field_option_fieldId_position_idx" ON "public"."field_opti...
type "public" (line 72) | CREATE UNIQUE INDEX "field_option_fieldId_value_key" ON "public"."field_...
type "public" (line 75) | CREATE UNIQUE INDEX "field_option_id_workspaceId_key" ON "public"."field...
type "public" (line 78) | CREATE INDEX "field_value_postId_idx" ON "public"."field_value"("postId")
type "public" (line 81) | CREATE INDEX "field_value_fieldId_idx" ON "public"."field_value"("fieldId")
type "public" (line 84) | CREATE INDEX "field_value_workspaceId_idx" ON "public"."field_value"("wo...
type "public" (line 87) | CREATE UNIQUE INDEX "field_value_postId_fieldId_key" ON "public"."field_...
type "public" (line 90) | CREATE UNIQUE INDEX "post_id_workspaceId_key" ON "public"."post"("id", "...
FILE: packages/db/prisma/migrations/20260508223056_add_notification_preferences/migration.sql
type "user_notification_preferences" (line 21) | CREATE TABLE "user_notification_preferences" (
type "workspace_notification_preferences" (line 36) | CREATE TABLE "workspace_notification_preferences" (
type "user_notification_preferences" (line 48) | CREATE UNIQUE INDEX "user_notification_preferences_userId_key" ON "user_...
type "workspace_notification_preferences" (line 51) | CREATE UNIQUE INDEX "workspace_notification_preferences_memberId_key" ON...
FILE: packages/db/prisma/migrations/20260513192507_add_workspace_events/migration.sql
type "usage_alert" (line 33) | CREATE TABLE "usage_alert" (
type "workspace_event" (line 47) | CREATE TABLE "workspace_event" (
type "webhook_delivery" (line 64) | CREATE TABLE "webhook_delivery" (
type "webhook_delivery_attempt" (line 85) | CREATE TABLE "webhook_delivery_attempt" (
type "usage_alert" (line 100) | CREATE INDEX "usage_alert_workspaceId_type_periodStart_periodEnd_idx" ON...
type "usage_alert" (line 103) | CREATE UNIQUE INDEX "usage_alert_workspaceId_type_kind_periodStart_perio...
type "workspace_event" (line 106) | CREATE INDEX "workspace_event_workspaceId_createdAt_idx" ON "workspace_e...
type "workspace_event" (line 109) | CREATE INDEX "workspace_event_workspaceId_type_idx" ON "workspace_event"...
type "workspace_event" (line 112) | CREATE INDEX "workspace_event_workspaceId_resourceType_resourceId_idx" O...
type "workspace_event" (line 115) | CREATE INDEX "workspace_event_workspaceId_processedAt_idx" ON "workspace...
type "webhook_delivery" (line 118) | CREATE INDEX "webhook_delivery_eventId_idx" ON "webhook_delivery"("event...
type "webhook_delivery" (line 121) | CREATE INDEX "webhook_delivery_workspaceId_status_idx" ON "webhook_deliv...
type "webhook_delivery" (line 124) | CREATE INDEX "webhook_delivery_workspaceId_createdAt_idx" ON "webhook_de...
type "webhook_delivery" (line 127) | CREATE INDEX "webhook_delivery_webhookEndpointId_idx" ON "webhook_delive...
type "webhook_delivery" (line 130) | CREATE UNIQUE INDEX "webhook_delivery_eventId_webhookEndpointId_key" ON ...
type "webhook_delivery_attempt" (line 133) | CREATE INDEX "webhook_delivery_attempt_deliveryId_idx" ON "webhook_deliv...
type "webhook_delivery_attempt" (line 136) | CREATE UNIQUE INDEX "webhook_delivery_attempt_deliveryId_attemptNumber_k...
FILE: packages/editor/src/components/color-picker.tsx
constant PRESET_COLORS (line 8) | const PRESET_COLORS = [
FILE: packages/editor/src/components/editor-character-count.tsx
type EditorCharacterCountProps (line 5) | interface EditorCharacterCountProps {
method Characters (line 23) | Characters({ children, className }: EditorCharacterCountProps) {
method Words (line 43) | Words({ children, className }: EditorCharacterCountProps) {
FILE: packages/editor/src/components/editor-content.tsx
function EditorContent (line 14) | function EditorContent() {
FILE: packages/editor/src/components/editor-provider.tsx
function deduplicateExtensions (line 12) | function deduplicateExtensions(
type EditorProviderProps (line 23) | type EditorProviderProps = Omit<
function useMarbleEditor (line 104) | function useMarbleEditor(options: UseMarbleEditorOptions) {
type UseMarbleEditorOptions (line 121) | type UseMarbleEditorOptions = Omit<UseEditorOptions, "extensions"> & {
FILE: packages/editor/src/components/editor-table-menus.tsx
function EditorTableMenus (line 22) | function EditorTableMenus() {
FILE: packages/editor/src/components/marks/editor-clear-formatting.tsx
type EditorClearFormattingProps (line 18) | type EditorClearFormattingProps = Pick<EditorButtonProps, "hideName">;
FILE: packages/editor/src/components/marks/editor-link-selector.tsx
type EditorLinkSelectorProps (line 26) | interface EditorLinkSelectorProps {
FILE: packages/editor/src/components/marks/editor-mark-bold.tsx
type EditorMarkBoldProps (line 6) | type EditorMarkBoldProps = Pick<EditorButtonProps, "hideName">;
FILE: packages/editor/src/components/marks/editor-mark-code.tsx
type EditorMarkCodeProps (line 6) | type EditorMarkCodeProps = Pick<EditorButtonProps, "hideName">;
FILE: packages/editor/src/components/marks/editor-mark-highlight.tsx
type EditorMarkHighlightProps (line 14) | type EditorMarkHighlightProps = Pick<EditorButtonProps, "hideName">;
FILE: packages/editor/src/components/marks/editor-mark-italic.tsx
type EditorMarkItalicProps (line 6) | type EditorMarkItalicProps = Pick<EditorButtonProps, "hideName">;
FILE: packages/editor/src/components/marks/editor-mark-strike.tsx
type EditorMarkStrikeProps (line 6) | type EditorMarkStrikeProps = Pick<EditorButtonProps, "hideName">;
FILE: packages/editor/src/components/marks/editor-mark-subscript.tsx
type EditorMarkSubscriptProps (line 6) | type EditorMarkSubscriptProps = Pick<EditorButtonProps, "hideName">;
FILE: packages/editor/src/components/marks/editor-mark-superscript.tsx
type EditorMarkSuperscriptProps (line 6) | type EditorMarkSuperscriptProps = Pick<EditorButtonProps, "hideName">;
FILE: packages/editor/src/components/marks/editor-mark-text-color.tsx
type EditorMarkTextColorProps (line 14) | type EditorMarkTextColorProps = Pick<EditorButtonProps, "hideName">;
FILE: packages/editor/src/components/marks/editor-mark-underline.tsx
type EditorMarkUnderlineProps (line 6) | type EditorMarkUnderlineProps = Pick<EditorButtonProps, "hideName">;
FILE: packages/editor/src/components/menus/block-handle-menu.tsx
type TargetBlock (line 56) | interface TargetBlock {
type TransformOption (line 61) | interface TransformOption {
type EditorBlockHandleMenuProps (line 68) | interface EditorBlockHandleMenuProps {
constant HANDLE_PLUGIN_KEY (line 72) | const HANDLE_PLUGIN_KEY = "marble-block-handle";
constant SUPPORTED_NODE_TYPES (line 74) | const SUPPORTED_NODE_TYPES = new Set([
constant TURN_INTO_SOURCE_TYPES (line 94) | const TURN_INTO_SOURCE_TYPES = new Set([
constant CLEAR_FORMATTING_TYPES (line 101) | const CLEAR_FORMATTING_TYPES = new Set([
constant HANDLE_CONTROL_CLASSNAME (line 108) | const HANDLE_CONTROL_CLASSNAME =
function getFocusPos (line 111) | function getFocusPos(target: TargetBlock) {
function isSupportedNode (line 115) | function isSupportedNode(
function canTurnInto (line 121) | function canTurnInto(node: ProseMirrorNode) {
function canClearFormatting (line 125) | function canClearFormatting(node: ProseMirrorNode) {
function getScrollParent (line 129) | function getScrollParent(node: HTMLElement | null) {
function serializeNodeToClipboardData (line 149) | function serializeNodeToClipboardData(
function EditorBlockHandleMenu (line 168) | function EditorBlockHandleMenu({
FILE: packages/editor/src/components/menus/bubble-menu.tsx
type EditorBubbleMenuProps (line 11) | type EditorBubbleMenuProps = Omit<TiptapBubbleMenuProps, "editor">;
FILE: packages/editor/src/components/menus/floating-menu.tsx
type EditorFloatingMenuProps (line 8) | type EditorFloatingMenuProps = Omit<TiptapFloatingMenuProps, "editor">;
FILE: packages/editor/src/components/nodes/editor-align-selector.tsx
type EditorAlignSelectorProps (line 22) | interface EditorAlignSelectorProps {
type Alignment (line 27) | type Alignment = "left" | "center" | "right" | "justify";
FILE: packages/editor/src/components/nodes/editor-align.tsx
type EditorAlignProps (line 22) | type EditorAlignProps = Pick<EditorButtonProps, "hideName">;
FILE: packages/editor/src/components/nodes/editor-node-bullet-list.tsx
type EditorNodeBulletListProps (line 6) | type EditorNodeBulletListProps = Pick<EditorButtonProps, "hideName">;
FILE: packages/editor/src/components/nodes/editor-node-code.tsx
type EditorNodeCodeProps (line 6) | type EditorNodeCodeProps = Pick<EditorButtonProps, "hideName">;
FILE: packages/editor/src/components/nodes/editor-node-heading1.tsx
type EditorNodeHeading1Props (line 6) | type EditorNodeHeading1Props = Pick<EditorButtonProps, "hideName">;
FILE: packages/editor/src/components/nodes/editor-node-heading2.tsx
type EditorNodeHeading2Props (line 6) | type EditorNodeHeading2Props = Pick<EditorButtonProps, "hideName">;
FILE: packages/editor/src/components/nodes/editor-node-heading3.tsx
type EditorNodeHeading3Props (line 6) | type EditorNodeHeading3Props = Pick<EditorButtonProps, "hideName">;
FILE: packages/editor/src/components/nodes/editor-node-ordered-list.tsx
type EditorNodeOrderedListProps (line 6) | type EditorNodeOrderedListProps = Pick<EditorButtonProps, "hideName">;
FILE: packages/editor/src/components/nodes/editor-node-quote.tsx
type EditorNodeQuoteProps (line 6) | type EditorNodeQuoteProps = Pick<EditorButtonProps, "hideName">;
FILE: packages/editor/src/components/nodes/editor-node-table.tsx
type EditorNodeTableProps (line 6) | type EditorNodeTableProps = Pick<EditorButtonProps, "hideName">;
FILE: packages/editor/src/components/nodes/editor-node-task-list.tsx
type EditorNodeTaskListProps (line 6) | type EditorNodeTaskListProps = Pick<EditorButtonProps, "hideName">;
FILE: packages/editor/src/components/nodes/editor-node-text.tsx
type EditorNodeTextProps (line 6) | type EditorNodeTextProps = Pick<EditorButtonProps, "hideName">;
FILE: packages/editor/src/components/rich-text-field.tsx
type FieldRichTextEditorProps (line 18) | interface FieldRichTextEditorProps {
function ToolbarButton (line 28) | function ToolbarButton({
function FieldRichTextEditor (line 56) | function FieldRichTextEditor({
FILE: packages/editor/src/components/ui/editor-selector.tsx
type EditorSelectorProps (line 12) | type EditorSelectorProps = HTMLAttributes<HTMLDivElement> & {
FILE: packages/editor/src/extensions/code-block/code-block-comp.tsx
constant LANGUAGES (line 24) | const LANGUAGES = [
constant LANGUAGE_ALIASES (line 39) | const LANGUAGE_ALIASES: Record<string, string> = {
constant LANGUAGE_VALUES (line 56) | const LANGUAGE_VALUES: Set<string> = new Set(LANGUAGES.map((l) => l.valu...
type CodeBlockCompProps (line 70) | interface CodeBlockCompProps {
FILE: packages/editor/src/extensions/code-block/code-block.ts
method addNodeView (line 20) | addNodeView() {
method addInputRules (line 24) | addInputRules() {
FILE: packages/editor/src/extensions/extension-kit.ts
type ExtensionKitOptions (line 31) | interface ExtensionKitOptions {
FILE: packages/editor/src/extensions/figure/index.ts
type Commands (line 7) | interface Commands<ReturnType> {
method addAttributes (line 36) | addAttributes() {
method parseHTML (line 98) | parseHTML() {
method renderHTML (line 113) | renderHTML({ HTMLAttributes }) {
method addCommands (line 147) | addCommands() {
method addNodeView (line 171) | addNodeView() {
FILE: packages/editor/src/extensions/image-upload/image-upload-comp.tsx
type ImageUploadCompProps (line 36) | interface ImageUploadCompProps {
FILE: packages/editor/src/extensions/image-upload/index.ts
type Commands (line 9) | interface Commands<ReturnType> {
method addOptions (line 25) | addOptions() {
method addAttributes (line 37) | addAttributes() {
method parseHTML (line 54) | parseHTML() {
method renderHTML (line 62) | renderHTML({ HTMLAttributes }) {
method addCommands (line 66) | addCommands() {
method addNodeView (line 91) | addNodeView() {
method addStorage (line 97) | addStorage() {
method onDestroy (line 104) | onDestroy() {
type ImageUploadStorage (line 110) | interface ImageUploadStorage {
FILE: packages/editor/src/extensions/markdown-input/index.ts
method addProseMirrorPlugins (line 16) | addProseMirrorPlugins() {
FILE: packages/editor/src/extensions/markdown-input/utils.ts
function looksLikeMarkdown (line 6) | function looksLikeMarkdown(text: string): boolean {
function isInlineContext (line 38) | function isInlineContext(nodeType?: string): boolean {
function transformImageToFigure (line 48) | function transformImageToFigure(
function liftFiguresFromParagraphs (line 118) | function liftFiguresFromParagraphs(content: JSONContent): JSONContent {
function transformContent (line 148) | function transformContent(
FILE: packages/editor/src/extensions/slash-command/slash-command.ts
type SlashOptions (line 24) | interface SlashOptions<
method addOptions (line 52) | addOptions() {
method addAttributes (line 161) | addAttributes() {
method parseHTML (line 193) | parseHTML() {
method renderHTML (line 201) | renderHTML({ node, HTMLAttributes }) {
method renderText (line 228) | renderText({ node }) {
method addKeyboardShortcuts (line 235) | addKeyboardShortcuts() {
method addProseMirrorPlugins (line 267) | addProseMirrorPlugins() {
method onUpdate (line 355) | onUpdate(onUpdateProps) {
method onKeyDown (line 359) | onKeyDown(onKeyDownProps) {
method onExit (line 375) | onExit() {
FILE: packages/editor/src/extensions/table/menus/table-column/index.tsx
type MenuProps (line 12) | interface MenuProps {
type ShouldShowProps (line 17) | interface ShouldShowProps {
function TableColumnMenuComponent (line 23) | function TableColumnMenuComponent({
FILE: packages/editor/src/extensions/table/menus/table-row/index.tsx
type MenuProps (line 10) | interface MenuProps {
type ShouldShowProps (line 15) | interface ShouldShowProps {
function TableRowMenuComponent (line 21) | function TableRowMenuComponent({ editor, appendTo }: MenuProps): JSX.Ele...
FILE: packages/editor/src/extensions/table/table-cell.ts
type TableCellOptions (line 7) | interface TableCellOptions {
method addOptions (line 20) | addOptions() {
method parseHTML (line 26) | parseHTML() {
method renderHTML (line 30) | renderHTML({ HTMLAttributes }) {
method addAttributes (line 38) | addAttributes() {
method addProseMirrorPlugins (line 73) | addProseMirrorPlugins() {
FILE: packages/editor/src/extensions/table/table-header.ts
method addAttributes (line 8) | addAttributes() {
method addProseMirrorPlugins (line 35) | addProseMirrorPlugins() {
FILE: packages/editor/src/extensions/twitter/index.tsx
constant TWITTER_REGEX_GLOBAL (line 9) | const TWITTER_REGEX_GLOBAL =
constant TWITTER_REGEX (line 11) | const TWITTER_REGEX =
type TwitterOptions (line 37) | interface TwitterOptions {
type SetTweetOptions (line 66) | type SetTweetOptions = { src: string };
type Commands (line 69) | interface Commands<ReturnType> {
method addOptions (line 87) | addOptions() {
method addNodeView (line 96) | addNodeView() {
method inline (line 102) | inline() {
method group (line 106) | group() {
method addAttributes (line 112) | addAttributes() {
method parseHTML (line 129) | parseHTML() {
method addCommands (line 137) | addCommands() {
method addPasteRules (line 154) | addPasteRules() {
method renderHTML (line 168) | renderHTML({ HTMLAttributes }) {
FILE: packages/editor/src/extensions/twitter/twitter-comp.tsx
constant TWITTER_REGEX (line 16) | const TWITTER_REGEX =
function isValidTwitterUrl (line 19) | function isValidTwitterUrl(url: string): boolean {
FILE: packages/editor/src/extensions/twitter/twitter-upload.ts
method addNodeView (line 17) | addNodeView() {
method parseHTML (line 21) | parseHTML() {
method renderHTML (line 29) | renderHTML() {
FILE: packages/editor/src/extensions/video-upload/index.ts
type Commands (line 9) | interface Commands<ReturnType> {
method addOptions (line 25) | addOptions() {
method addAttributes (line 37) | addAttributes() {
method parseHTML (line 54) | parseHTML() {
method renderHTML (line 62) | renderHTML({ HTMLAttributes }) {
method addCommands (line 66) | addCommands() {
method addNodeView (line 91) | addNodeView() {
method addStorage (line 97) | addStorage() {
method onDestroy (line 104) | onDestroy() {
type VideoUploadStorage (line 110) | interface VideoUploadStorage {
FILE: packages/editor/src/extensions/video-upload/video-upload-comp.tsx
type VideoUploadCompProps (line 36) | interface VideoUploadCompProps {
FILE: packages/editor/src/extensions/video/index.ts
type Commands (line 7) | interface Commands<ReturnType> {
method addAttributes (line 32) | addAttributes() {
method parseHTML (line 68) | parseHTML() {
method renderHTML (line 94) | renderHTML({ HTMLAttributes }) {
method addCommands (line 112) | addCommands() {
method addNodeView (line 136) | addNodeView() {
FILE: packages/editor/src/extensions/youtube/youtube-comp.tsx
function extractYouTubeVideoId (line 16) | function extractYouTubeVideoId(url: string): string | null {
FILE: packages/editor/src/extensions/youtube/youtube-upload.ts
method addNodeView (line 17) | addNodeView() {
method parseHTML (line 21) | parseHTML() {
method renderHTML (line 29) | renderHTML() {
FILE: packages/editor/src/lib/utils.ts
function isTableGripSelected (line 7) | function isTableGripSelected(node: HTMLElement): boolean {
function isCustomNodeSelected (line 29) | function isCustomNodeSelected(
function isTextSelected (line 60) | function isTextSelected({ editor }: { editor: Editor | null }): boolean {
FILE: packages/editor/src/types/index.ts
type EditorIcon (line 8) | type EditorIcon =
type SuggestionItem (line 16) | interface SuggestionItem {
type EditorProviderProps (line 27) | interface EditorProviderProps {
type EditorButtonProps (line 41) | interface EditorButtonProps {
type EditorSlashMenuProps (line 52) | interface EditorSlashMenuProps {
type SlashNodeAttrs (line 62) | interface SlashNodeAttrs {
type MediaItem (line 70) | interface MediaItem {
type MediaPage (line 80) | interface MediaPage {
type ImageUploadOptions (line 88) | interface ImageUploadOptions {
type VideoUploadOptions (line 108) | interface VideoUploadOptions {
FILE: packages/email/src/components/button.tsx
type EmailButtonProps (line 4) | type EmailButtonProps = ComponentProps<typeof ReactEmailButton>;
FILE: packages/email/src/emails/founder.tsx
type FounderEmailProps (line 15) | interface FounderEmailProps {
FILE: packages/email/src/emails/invite.tsx
type InviteUserEmailProps (line 20) | interface InviteUserEmailProps {
FILE: packages/email/src/emails/reset.tsx
type ResetPasswordProps (line 19) | interface ResetPasswordProps {
FILE: packages/email/src/emails/usage-limit.tsx
type UsageLimitEmailProps (line 20) | interface UsageLimitEmailProps {
function formatNumber (line 28) | function formatNumber(num: number): string {
FILE: packages/email/src/emails/verify.tsx
type VerifyUserEmailProps (line 18) | interface VerifyUserEmailProps {
FILE: packages/email/src/emails/welcome.tsx
type WelcomeEmailProps (line 20) | interface WelcomeEmailProps {
FILE: packages/email/src/lib/config.ts
constant EMAIL_CONFIG (line 1) | const EMAIL_CONFIG = {
method getSiteUrl (line 6) | getSiteUrl(): string {
method getAppUrl (line 14) | getAppUrl(): string {
method getLogoUrl (line 21) | getLogoUrl(): string {
FILE: packages/email/src/lib/dev.ts
type MockableEmailOptions (line 3) | type MockableEmailOptions = CreateEmailOptions & {
function sendDevEmail (line 16) | async function sendDevEmail(options: MockableEmailOptions) {
FILE: packages/email/src/lib/send.ts
type SendInviteEmailProps (line 10) | interface SendInviteEmailProps {
function sendInviteEmail (line 19) | async function sendInviteEmail(
function sendVerificationEmail (line 44) | async function sendVerificationEmail(
function sendResetPassword (line 69) | async function sendResetPassword(
function sendWelcomeEmail (line 91) | async function sendWelcomeEmail(
function sendUsageLimitEmail (line 111) | async function sendUsageLimitEmail(
function sendFounderEmail (line 144) | async function sendFounderEmail(
FILE: packages/events/src/types.ts
constant WORKSPACE_EVENT_TYPES (line 2) | const WORKSPACE_EVENT_TYPES = [
constant WORKSPACE_EVENT_SOURCES (line 23) | const WORKSPACE_EVENT_SOURCES = [
constant WORKSPACE_EVENT_ACTOR_TYPES (line 32) | const WORKSPACE_EVENT_ACTOR_TYPES = [
constant WORKSPACE_EVENT_RESOURCE_TYPES (line 40) | const WORKSPACE_EVENT_RESOURCE_TYPES = [
type WorkspaceEventType (line 49) | type WorkspaceEventType = (typeof WORKSPACE_EVENT_TYPES)[number];
type WorkspaceEventSource (line 50) | type WorkspaceEventSource = (typeof WORKSPACE_EVENT_SOURCES)[number];
type WorkspaceEventActorType (line 51) | type WorkspaceEventActorType =
type WorkspaceEventResourceType (line 53) | type WorkspaceEventResourceType =
type EventPayloadValue (line 56) | type EventPayloadValue =
type EventPayloadArray (line 64) | interface EventPayloadArray extends Array<EventPayloadValue> {}
type EventPayload (line 67) | interface EventPayload {
type WorkspaceEventLike (line 72) | interface WorkspaceEventLike {
type Dateish (line 84) | type Dateish = Date | string | null | undefined;
FILE: packages/events/src/utils/demo.ts
function getDemoPostPublishedPayload (line 4) | function getDemoPostPublishedPayload(): EventPayload {
FILE: packages/events/src/utils/envelope.ts
function buildWebhookPayload (line 5) | function buildWebhookPayload(event: WorkspaceEventLike) {
FILE: packages/events/src/utils/events.ts
function serializeEventType (line 4) | function serializeEventType(type: string) {
function getResourceTypeForEvent (line 9) | function getResourceTypeForEvent(
FILE: packages/events/src/utils/resources.ts
function serializeDate (line 4) | function serializeDate(value: Dateish) {
type AuthorInput (line 11) | interface AuthorInput {
function toAuthorPayload (line 25) | function toAuthorPayload(author: AuthorInput): EventPayload {
type CategoryInput (line 44) | interface CategoryInput {
function toCategoryPayload (line 54) | function toCategoryPayload(category: CategoryInput): EventPayload {
type TagInput (line 65) | interface TagInput {
function toTagPayload (line 75) | function toTagPayload(tag: TagInput): EventPayload {
type MediaInput (line 86) | interface MediaInput {
function toMediaPayload (line 103) | function toMediaPayload(media: MediaInput): EventPayload {
type PostInput (line 121) | interface PostInput {
function toPostPayload (line 137) | function toPostPayload(post: PostInput): EventPayload {
function withChanges (line 155) | function withChanges(
FILE: packages/parser/src/tiptap.ts
class MarkdownToTiptapParser (line 4) | class MarkdownToTiptapParser {
method constructor (line 7) | constructor() {
method parse (line 11) | parse(markdown: string): JSONContent {
method parseTokens (line 16) | private parseTokens(tokens: Token[]): JSONContent[] {
method parseToken (line 31) | private parseToken(token: Token): JSONContent | JSONContent[] | null {
method parseHeading (line 58) | static parseHeading(token: Tokens.Heading): JSONContent {
method parseParagraph (line 66) | static parseParagraph(token: Tokens.Paragraph): JSONContent {
method parseBlockquote (line 73) | static parseBlockquote(token: Tokens.Blockquote): JSONContent {
method parseList (line 81) | static parseList(token: Tokens.List): JSONContent {
method parseTaskListItem (line 112) | static parseTaskListItem(item: Tokens.ListItem): JSONContent {
method parseListItem (line 122) | static parseListItem(item: Tokens.ListItem): JSONContent {
method parseCodeBlock (line 161) | static parseCodeBlock(token: Tokens.Code): JSONContent {
method parseTable (line 169) | static parseTable(token: Tokens.Table): JSONContent {
method parseHTML (line 223) | static parseHTML(token: Tokens.HTML): JSONContent | null {
method parseInlineTokens (line 237) | static parseInlineTokens(tokens: Token[]): JSONContent[] {
method parseInlineToken (line 252) | static parseInlineToken(token: Token): JSONContent | JSONContent[] | n...
method parseStrong (line 275) | static parseStrong(token: Tokens.Strong): JSONContent[] {
method parseEm (line 285) | static parseEm(token: Tokens.Em): JSONContent[] {
method parseCodespan (line 295) | static parseCodespan(token: Tokens.Codespan): JSONContent {
method parseDel (line 303) | static parseDel(token: Tokens.Del): JSONContent[] {
method parseLink (line 313) | static parseLink(token: Tokens.Link): JSONContent[] {
method parseImage (line 326) | static parseImage(token: Tokens.Image): JSONContent {
function markdownToTiptap (line 334) | function markdownToTiptap(markdown: string): JSONContent {
function markdownToHtml (line 339) | async function markdownToHtml(markdown: string): Promise<string> {
FILE: packages/ui/src/components/alert-dialog.tsx
function AlertDialog (line 10) | function AlertDialog({ ...props }: AlertDialogPrimitive.Root.Props) {
function AlertDialogTrigger (line 14) | function AlertDialogTrigger({ ...props }: AlertDialogPrimitive.Trigger.P...
function AlertDialogPortal (line 20) | function AlertDialogPortal({ ...props }: AlertDialogPrimitive.Portal.Pro...
function AlertDialogOverlay (line 26) | function AlertDialogOverlay({
function AlertDialogContent (line 49) | function AlertDialogContent({
function AlertDialogHeader (line 76) | function AlertDialogHeader({
function AlertDialogBody (line 89) | function AlertDialogBody({
function AlertDialogFooter (line 102) | function AlertDialogFooter({
function AlertDialogMedia (line 118) | function AlertDialogMedia({
function AlertDialogTitle (line 134) | function AlertDialogTitle({
function AlertDialogDescription (line 147) | function AlertDialogDescription({
function AlertDialogAction (line 160) | function AlertDialogAction({
function AlertDialogCancel (line 173) | function AlertDialogCancel({
function AlertDialogX (line 189) | function AlertDialogX({
FILE: packages/ui/src/components/avatar.tsx
function Avatar (line 8) | function Avatar({
function AvatarImage (line 28) | function AvatarImage({ className, ...props }: AvatarPrimitive.Image.Prop...
function AvatarFallback (line 41) | function AvatarFallback({
function AvatarBadge (line 57) | function AvatarBadge({ className, ...props }: React.ComponentProps<"span...
function AvatarGroup (line 73) | function AvatarGroup({ className, ...props }: React.ComponentProps<"div"...
function AvatarGroupCount (line 86) | function AvatarGroupCount({
FILE: packages/ui/src/components/badge.tsx
function Badge (line 43) | function Badge({
FILE: packages/ui/src/components/breadcrumb.tsx
function Breadcrumb (line 14) | function Breadcrumb({ className, ...props }: React.ComponentProps<"nav">) {
function BreadcrumbList (line 25) | function BreadcrumbList({ className, ...props }: React.ComponentProps<"o...
function BreadcrumbItem (line 38) | function BreadcrumbItem({ className, ...props }: React.ComponentProps<"l...
function BreadcrumbLink (line 48) | function BreadcrumbLink({
function BreadcrumbPage (line 68) | function BreadcrumbPage({ className, ...props }: React.ComponentProps<"s...
function BreadcrumbSeparator (line 81) | function BreadcrumbSeparator({
function BreadcrumbEllipsis (line 99) | function BreadcrumbEllipsis({
FILE: packages/ui/src/components/button.tsx
type ButtonProps (line 8) | type ButtonProps = ButtonPrimitive.Props &
function Button (line 48) | function Button({
FILE: packages/ui/src/components/calendar.tsx
function Calendar (line 19) | function Calendar({
function CalendarDayButton (line 198) | function CalendarDayButton({
FILE: packages/ui/src/components/card.tsx
function Card (line 5) | function Card({ className, ...props }: React.ComponentProps<"div">) {
function CardHeader (line 18) | function CardHeader({ className, ...props }: React.ComponentProps<"div">) {
function CardTitle (line 31) | function CardTitle({ className, ...props }: React.ComponentProps<"div">) {
function CardDescription (line 41) | function CardDescription({ className, ...props }: React.ComponentProps<"...
function CardAction (line 51) | function CardAction({ className, ...props }: React.ComponentProps<"div">) {
function CardContent (line 64) | function CardContent({ className, ...props }: React.ComponentProps<"div"...
function CardFooter (line 74) | function CardFooter({ className, ...props }: React.ComponentProps<"div">) {
FILE: packages/ui/src/components/chart.tsx
constant THEMES (line 9) | const THEMES = { light: "", dark: ".dark" } as const
type ChartConfig (line 11) | type ChartConfig = {
type ChartContextProps (line 21) | type ChartContextProps = {
function useChart (line 27) | function useChart() {
function ChartContainer (line 37) | function ChartContainer({
function ChartTooltipContent (line 107) | function ChartTooltipContent({
function ChartLegendContent (line 253) | function ChartLegendContent({
function getPayloadConfigFromPayload (line 308) | function getPayloadConfigFromPayload(
FILE: packages/ui/src/components/checkbox.tsx
function Checkbox (line 8) | function Checkbox({ className, ...props }: CheckboxPrimitive.Root.Props) {
FILE: packages/ui/src/components/collapsible.tsx
function Collapsible (line 5) | function Collapsible({ ...props }: CollapsiblePrimitive.Root.Props) {
function CollapsibleTrigger (line 9) | function CollapsibleTrigger({ ...props }: CollapsiblePrimitive.Trigger.P...
function CollapsibleContent (line 15) | function CollapsibleContent({ ...props }: CollapsiblePrimitive.Panel.Pro...
FILE: packages/ui/src/components/command.tsx
function Command (line 17) | function Command({
function CommandDialog (line 33) | function CommandDialog({
function CommandInput (line 65) | function CommandInput({
function CommandList (line 87) | function CommandList({
function CommandEmpty (line 103) | function CommandEmpty({
function CommandGroup (line 115) | function CommandGroup({
function CommandSeparator (line 131) | function CommandSeparator({
function CommandItem (line 144) | function CommandItem({
function CommandShortcut (line 160) | function CommandShortcut({
FILE: packages/ui/src/components/dialog.tsx
function Dialog (line 10) | function Dialog({ ...props }: DialogPrimitive.Root.Props) {
function DialogTrigger (line 14) | function DialogTrigger({ ...props }: DialogPrimitive.Trigger.Props) {
function DialogPortal (line 18) | function DialogPortal({ ...props }: DialogPrimitive.Portal.Props) {
function DialogClose (line 22) | function DialogClose({
function DialogOverlay (line 39) | function DialogOverlay({
type DialogContentProps (line 55) | interface DialogContentProps extends DialogPrimitive.Popup.Props {
function DialogContent (line 60) | function DialogContent({
function DialogHeader (line 101) | function DialogHeader({ className, ...props }: React.ComponentProps<"div...
function DialogBody (line 111) | function DialogBody({ className, ...props }: React.ComponentProps<"div">) {
function DialogFooter (line 121) | function DialogFooter({ className, ...props }: React.ComponentProps<"div...
function DialogTitle (line 134) | function DialogTitle({ className, ...props }: DialogPrimitive.Title.Prop...
function DialogDescription (line 144) | function DialogDescription({
function DialogX (line 157) | function DialogX({
FILE: packages/ui/src/components/drawer.tsx
function Drawer (line 8) | function Drawer({
function DrawerTrigger (line 14) | function DrawerTrigger({
function DrawerPortal (line 20) | function DrawerPortal({
function DrawerClose (line 26) | function DrawerClose({
function DrawerOverlay (line 32) | function DrawerOverlay({
function DrawerContent (line 48) | function DrawerContent({
function DrawerHeader (line 75) | function DrawerHeader({ className, ...props }: React.ComponentProps<"div...
function DrawerFooter (line 88) | function DrawerFooter({ className, ...props }: React.ComponentProps<"div...
function DrawerTitle (line 98) | function DrawerTitle({
function DrawerDescription (line 111) | function DrawerDescription({
FILE: packages/ui/src/components/dropdown-menu.tsx
function DropdownMenu (line 9) | function DropdownMenu({ ...props }: MenuPrimitive.Root.Props) {
function DropdownMenuPortal (line 13) | function DropdownMenuPortal({ ...props }: MenuPrimitive.Portal.Props) {
function DropdownMenuTrigger (line 17) | function DropdownMenuTrigger({ ...props }: MenuPrimitive.Trigger.Props) {
function createDropdownMenuHandle (line 21) | function createDropdownMenuHandle() {
function DropdownMenuContent (line 25) | function DropdownMenuContent({
function DropdownMenuGroup (line 59) | function DropdownMenuGroup({ ...props }: MenuPrimitive.Group.Props) {
function DropdownMenuLabel (line 63) | function DropdownMenuLabel({
function DropdownMenuItem (line 83) | function DropdownMenuItem({
function DropdownMenuSub (line 106) | function DropdownMenuSub({ ...props }: MenuPrimitive.SubmenuRoot.Props) {
function DropdownMenuSubTrigger (line 110) | function DropdownMenuSubTrigger({
function DropdownMenuSubContent (line 138) | function DropdownMenuSubContent({
function DropdownMenuCheckboxItem (line 162) | function DropdownMenuCheckboxItem({
function DropdownMenuRadioGroup (line 191) | function DropdownMenuRadioGroup({ ...props }: MenuPrimitive.RadioGroup.P...
function DropdownMenuRadioItem (line 200) | function DropdownMenuRadioItem({
function DropdownMenuSeparator (line 227) | function DropdownMenuSeparator({
function DropdownMenuShortcut (line 240) | function DropdownMenuShortcut({
FILE: packages/ui/src/components/input-otp.tsx
function InputOTP (line 10) | function InputOTP({
function InputOTPGroup (line 30) | function InputOTPGroup({ className, ...props }: React.ComponentProps<"di...
function InputOTPSlot (line 40) | function InputOTPSlot({
function InputOTPSeparator (line 74) | function InputOTPSeparator({ ...props }: React.ComponentProps<"div">) {
FILE: packages/ui/src/components/input.tsx
function Input (line 5) | function Input({ className, type, ...props }: React.ComponentProps<"inpu...
FILE: packages/ui/src/components/kibo-ui/contribution-graph/index.tsx
type Activity (line 26) | type Activity = {
type Week (line 32) | type Week = Array<Activity | undefined>;
type Labels (line 34) | type Labels = {
type MonthLabel (line 44) | type MonthLabel = {
constant DEFAULT_MONTH_LABELS (line 49) | const DEFAULT_MONTH_LABELS = [
constant DEFAULT_LABELS (line 64) | const DEFAULT_LABELS: Labels = {
type ContributionGraphContextType (line 74) | type ContributionGraphContextType = {
type ContributionGraphProps (line 225) | type ContributionGraphProps = HTMLAttributes<HTMLDivElement> & {
type ContributionGraphBlockProps (line 306) | type ContributionGraphBlockProps = HTMLAttributes<SVGRectElement> & {
type ContributionGraphCalendarProps (line 352) | type ContributionGraphCalendarProps = Omit<
type ContributionGraphFooterProps (line 422) | type ContributionGraphFooterProps = HTMLAttributes<HTMLDivElement>;
type ContributionGraphTotalCountProps (line 437) | type ContributionGraphTotalCountProps = Omit<
type ContributionGraphLegendProps (line 466) | type ContributionGraphLegendProps = Omit<
FILE: packages/ui/src/components/kibo-ui/image-crop/index.tsx
type ImageCropContextType (line 102) | type ImageCropContextType = {
type ImageCropProps (line 131) | type ImageCropProps = {
type ImageCropContentProps (line 232) | type ImageCropContentProps = {
type ImageCropApplyProps (line 278) | type ImageCropApplyProps = useRender.ComponentProps<"button"> & {
type ImageCropResetProps (line 326) | type ImageCropResetProps = useRender.ComponentProps<"button"> & {
type CropperProps (line 375) | type CropperProps = Omit<ReactCropProps, "onChange"> & {
FILE: packages/ui/src/components/label.tsx
function Label (line 7) | function Label({ className, ...props }: React.ComponentProps<"label">) {
FILE: packages/ui/src/components/pagination.tsx
function Pagination (line 14) | function Pagination({ className, ...props }: React.ComponentProps<"nav">) {
function PaginationContent (line 25) | function PaginationContent({
function PaginationItem (line 38) | function PaginationItem({ ...props }: React.ComponentProps<"li">) {
type PaginationLinkProps (line 42) | type PaginationLinkProps = {
function PaginationLink (line 47) | function PaginationLink({
function PaginationPrevious (line 70) | function PaginationPrevious({
function PaginationNext (line 92) | function PaginationNext({
function PaginationEllipsis (line 114) | function PaginationEllipsis({
FILE: packages/ui/src/components/popover.tsx
function Popover (line 8) | function Popover({ ...props }: PopoverPrimitive.Root.Props) {
function PopoverTrigger (line 12) | function PopoverTrigger({ ...props }: PopoverPrimitive.Trigger.Props) {
function PopoverContent (line 17) | function PopoverContent({
function PopoverHeader (line 53) | function PopoverHeader({ className, ...props }: React.ComponentProps<"di...
function PopoverTitle (line 63) | function PopoverTitle({ className, ...props }: PopoverPrimitive.Title.Pr...
function PopoverDescription (line 73) | function PopoverDescription({
FILE: packages/ui/src/components/progress.tsx
function Progress (line 7) | function Progress({
function ProgressTrack (line 28) | function ProgressTrack({ className, ...props }: ProgressPrimitive.Track....
function ProgressIndicator (line 41) | function ProgressIndicator({
function ProgressLabel (line 54) | function ProgressLabel({ className, ...props }: ProgressPrimitive.Label....
function ProgressValue (line 64) | function ProgressValue({ className, ...props }: ProgressPrimitive.Value....
FILE: packages/ui/src/components/radio-group.tsx
function RadioGroup (line 9) | function RadioGroup({ className, ...props }: RadioGroupPrimitive.Props) {
function RadioGroupItem (line 19) | function RadioGroupItem({ className, ...props }: RadioPrimitive.Root.Pro...
FILE: packages/ui/src/components/scroll-area.tsx
function ScrollArea (line 8) | function ScrollArea({
function ScrollBar (line 31) | function ScrollBar({
FILE: packages/ui/src/components/select.tsx
function SelectGroup (line 16) | function SelectGroup({ className, ...props }: SelectPrimitive.Group.Prop...
function SelectValue (line 26) | function SelectValue({ className, ...props }: SelectPrimitive.Value.Prop...
function SelectTrigger (line 36) | function SelectTrigger({
function SelectContent (line 64) | function SelectContent({
function SelectLabel (line 106) | function SelectLabel({
function SelectItem (line 119) | function SelectItem({
function SelectSeparator (line 151) | function SelectSeparator({
function SelectScrollUpButton (line 164) | function SelectScrollUpButton({
function SelectScrollDownButton (line 182) | function SelectScrollDownButton({
FILE: packages/ui/src/components/separator.tsx
function Separator (line 7) | function Separator({
FILE: packages/ui/src/components/sheet.tsx
function Sheet (line 10) | function Sheet({ ...props }: SheetPrimitive.Root.Props) {
function SheetTrigger (line 14) | function SheetTrigger({ ...props }: SheetPrimitive.Trigger.Props) {
function SheetClose (line 18) | function SheetClose({ ...props }: SheetPrimitive.Close.Props) {
function SheetPortal (line 22) | function SheetPortal({ ...props }: SheetPrimitive.Portal.Props) {
function SheetOverlay (line 26) | function SheetOverlay({ className, ...props }: SheetPrimitive.Backdrop.P...
function SheetContent (line 39) | function SheetContent({
function SheetHeader (line 78) | function SheetHeader({ className, ...props }: React.ComponentProps<"div"...
function SheetFooter (line 88) | function SheetFooter({ className, ...props }: React.ComponentProps<"div"...
function SheetTitle (line 98) | function SheetTitle({ className, ...props }: SheetPrimitive.Title.Props) {
function SheetDescription (line 108) | function SheetDescription({
function SheetX (line 121) | function SheetX({
FILE: packages/ui/src/components/sidebar.tsx
constant SIDEBAR_COOKIE_NAME (line 29) | const SIDEBAR_COOKIE_NAME = "sidebar_state";
constant SIDEBAR_COOKIE_MAX_AGE (line 30) | const SIDEBAR_COOKIE_MAX_AGE = 60 * 60 * 24 * 7;
constant SIDEBAR_WIDTH (line 31) | const SIDEBAR_WIDTH = "16rem";
constant SIDEBAR_WIDTH_MOBILE (line 32) | const SIDEBAR_WIDTH_MOBILE = "18rem";
constant SIDEBAR_WIDTH_ICON (line 33) | const SIDEBAR_WIDTH_ICON = "3rem";
constant SIDEBAR_KEYBOARD_SHORTCUT (line 34) | const SIDEBAR_KEYBOARD_SHORTCUT = "b";
function getSidebarKeyboardShortcutLabel (line 36) | function getSidebarKeyboardShortcutLabel() {
type SidebarContextProps (line 44) | type SidebarContextProps = {
function useSidebar (line 56) | function useSidebar() {
function SidebarProvider (line 65) | function SidebarProvider({
function Sidebar (line 164) | function Sidebar({
function SidebarTrigger (line 266) | function SidebarTrigger({
function SidebarRail (line 293) | function SidebarRail({ className, ...props }: React.ComponentProps<"butt...
function SidebarInset (line 319) | function SidebarInset({ className, ...props }: React.ComponentProps<"mai...
function SidebarInput (line 333) | function SidebarInput({
function SidebarHeader (line 347) | function SidebarHeader({ className, ...props }: React.ComponentProps<"di...
function SidebarFooter (line 358) | function SidebarFooter({ className, ...props }: React.ComponentProps<"di...
function SidebarSeparator (line 369) | function SidebarSeparator({
function SidebarContent (line 383) | function SidebarContent({ className, ...props }: React.ComponentProps<"d...
function SidebarGroup (line 397) | function SidebarGroup({ className, ...props }: React.ComponentProps<"div...
function SidebarGroupLabel (line 408) | function SidebarGroupLabel({
function SidebarGroupAction (line 433) | function SidebarGroupAction({
function SidebarGroupContent (line 460) | function SidebarGroupContent({
function SidebarMenu (line 474) | function SidebarMenu({ className, ...props }: React.ComponentProps<"ul">) {
function SidebarMenuItem (line 485) | function SidebarMenuItem({ className, ...props }: React.ComponentProps<"...
function SidebarMenuButton (line 518) | function SidebarMenuButton({
function SidebarMenuAction (line 582) | function SidebarMenuAction({
function SidebarMenuBadge (line 618) | function SidebarMenuBadge({
function SidebarMenuSkeleton (line 640) | function SidebarMenuSkeleton({
function SidebarMenuSub (line 678) | function SidebarMenuSub({ className, ...props }: React.ComponentProps<"u...
function SidebarMenuSubItem (line 693) | function SidebarMenuSubItem({
function SidebarMenuSubButton (line 707) | function SidebarMenuSubButton({
FILE: packages/ui/src/components/skeleton.tsx
function Skeleton (line 3) | function Skeleton({ className, ...props }: React.ComponentProps<"div">) {
FILE: packages/ui/src/components/switch.tsx
function Switch (line 7) | function Switch({
FILE: packages/ui/src/components/table.tsx
function Table (line 7) | function Table({ className, ...props }: React.ComponentProps<"table">) {
function TableHeader (line 22) | function TableHeader({ className, ...props }: React.ComponentProps<"thea...
function TableBody (line 32) | function TableBody({ className, ...props }: React.ComponentProps<"tbody"...
function TableFooter (line 42) | function TableFooter({ className, ...props }: React.ComponentProps<"tfoo...
function TableRow (line 55) | function TableRow({ className, ...props }: React.ComponentProps<"tr">) {
function TableHead (line 68) | function TableHead({ className, ...props }: React.ComponentProps<"th">) {
function TableCell (line 81) | function TableCell({ className, ...props }: React.ComponentProps<"td">) {
function TableCaption (line 94) | function TableCaption({
FILE: packages/ui/src/components/tabs.tsx
function Tabs (line 8) | function Tabs({
function TabsList (line 41) | function TabsList({
function TabsTrigger (line 68) | function TabsTrigger({ className, ...props }: TabsPrimitive.Tab.Props) {
function TabsContent (line 97) | function TabsContent({ className, ...props }: TabsPrimitive.Panel.Props) {
FILE: packages/ui/src/components/textarea.tsx
function Textarea (line 5) | function Textarea({ className, ...props }: React.ComponentProps<"textare...
FILE: packages/ui/src/components/toggle-group.tsx
function ToggleGroup (line 22) | function ToggleGroup({
function ToggleGroupItem (line 58) | function ToggleGroupItem({
FILE: packages/ui/src/components/toggle.tsx
function Toggle (line 29) | function Toggle({
FILE: packages/ui/src/components/tooltip.tsx
function TooltipProvider (line 7) | function TooltipProvider({
function Tooltip (line 20) | function Tooltip({ ...props }: TooltipPrimitive.Root.Props) {
function TooltipTrigger (line 28) | function TooltipTrigger({ ...props }: TooltipPrimitive.Trigger.Props) {
function TooltipContent (line 32) | function TooltipContent({
FILE: packages/ui/src/hooks/use-mobile.ts
constant MOBILE_BREAKPOINT (line 3) | const MOBILE_BREAKPOINT = 768
function useIsMobile (line 5) | function useIsMobile() {
FILE: packages/ui/src/lib/utils.ts
function cn (line 4) | function cn(...inputs: ClassValue[]) {
FILE: packages/utils/src/constants/api-key.ts
constant API_KEY_PREFIXES (line 1) | const API_KEY_PREFIXES = {
FILE: packages/utils/src/constants/plans.ts
type PlanType (line 1) | type PlanType = "pro" | "hobby";
type PlanLimits (line 3) | interface PlanLimits {
constant PLAN_LIMITS (line 16) | const PLAN_LIMITS: Record<PlanType, PlanLimits> = {
function isSubscriptionActive (line 46) | function isSubscriptionActive(
function getWorkspacePlan (line 79) | function getWorkspacePlan(
function canPerformAction (line 97) | function canPerformAction(
function canInviteMoreMembers (line 107) | function canInviteMoreMembers(
function getRemainingMemberSlots (line 120) | function getRemainingMemberSlots(
function getPlanLimits (line 130) | function getPlanLimits(plan: PlanType): PlanLimits {
function isOverLimit (line 137) | function isOverLimit(
FILE: packages/utils/src/constants/pricing.ts
type PricingPlan (line 1) | interface PricingPlan {
constant PRICING_PLANS (line 17) | const PRICING_PLANS: PricingPlan[] = [
FILE: packages/utils/src/constants/site.ts
constant YOUTUBE_VIDEO_ID (line 1) | const YOUTUBE_VIDEO_ID = "vAPVCCayBIA";
constant YOUTUBE_EMBED_URL (line 3) | const YOUTUBE_EMBED_URL = `https://www.youtube.com/embed/${YOUTUBE_VIDEO...
FILE: packages/utils/src/functions/api-key.ts
function hashApiKey (line 11) | function hashApiKey(key: string): string {
function verifyApiKey (line 22) | function verifyApiKey(plainKey: string, hashedKey: string): boolean {
function generateApiKey (line 44) | function generateApiKey(type: ApiKeyType) {
FILE: packages/utils/src/functions/highlight.ts
function getHighlighter (line 8) | async function getHighlighter() {
function highlightContent (line 50) | async function highlightContent(
FILE: packages/utils/src/functions/webhooks.ts
constant BLOCKED_HOSTNAMES (line 1) | const BLOCKED_HOSTNAMES = new Set(["localhost"]);
function parseIPv4 (line 3) | function parseIPv4(hostname: string) {
function isPrivateIPv4 (line 26) | function isPrivateIPv4(hostname: string) {
function normalizeIPv6 (line 49) | function normalizeIPv6(hostname: string) {
function isPrivateIPv6 (line 53) | function isPrivateIPv6(hostname: string) {
function isBlockedHostname (line 70) | function isBlockedHostname(hostname: string) {
function isSafeWebhookUrl (line 81) | function isSafeWebhookUrl(rawUrl: string) {
FILE: packages/utils/src/types/api-key.ts
type ApiKeyPrefix (line 3) | type ApiKeyPrefix =
type ApiKeyType (line 6) | type ApiKeyType = "public" | "private";
Condensed preview — 730 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (2,624K chars).
[
{
"path": ".cursor/rules/mintlify.mdc",
"chars": 10613,
"preview": "---\nalwaysApply: true\n---\n\n# Mintlify technical writing rule\n\nYou are an AI writing assistant specialized in creating ex"
},
{
"path": ".cursor/rules/ultracite.mdc",
"chars": 17028,
"preview": "---\ndescription: Ultracite Rules - AI-Ready Formatter and Linter\nglobs: \"**/*.{ts,tsx,js,jsx}\"\nalwaysApply: true\n---\n\n# "
},
{
"path": ".dockerignore",
"chars": 204,
"preview": ".git\nnode_modules\npnpm-store\n**/.next\n**/.turbo\n**/dist\n**/.output\n**/.vercel\n.env\n.env.*\n!.env.example\n!**/.env.example"
},
{
"path": ".github/CONTRIBUTING.md",
"chars": 12918,
"preview": "# Contributing to Marble\n\nThanks for your interest in contributing! This guide explains how to get Marble running locall"
},
{
"path": ".github/FUNDING.yml",
"chars": 20,
"preview": "github: [usemarble]\n"
},
{
"path": ".github/ISSUE_TEMPLATE/bug_report.md",
"chars": 834,
"preview": "---\nname: Bug report\nabout: Create a report to help us improve\ntitle: ''\nlabels: ''\nassignees: ''\n\n---\n\n**Describe the b"
},
{
"path": ".github/ISSUE_TEMPLATE/feature_request.md",
"chars": 595,
"preview": "---\nname: Feature request\nabout: Suggest an idea for this project\ntitle: ''\nlabels: ''\nassignees: ''\n\n---\n\n**Is your fea"
},
{
"path": ".github/pull_request_template.md",
"chars": 990,
"preview": "## Description\n\n<!--- Clearly describe what this PR changes. Include relevant details. -->\n\n## Motivation and Context\n\n<"
},
{
"path": ".github/workflows/code-quality.yml",
"chars": 416,
"preview": "name: Code quality\n\non:\n push:\n pull_request:\njobs:\n quality:\n runs-on: ubuntu-latest\n steps:\n - name: Che"
},
{
"path": ".github/workflows/tests.yml",
"chars": 429,
"preview": "name: Tests\n\non:\n push:\n pull_request:\n workflow_dispatch:\njobs:\n tests:\n runs-on: ubuntu-latest\n steps:\n "
},
{
"path": ".gitignore",
"chars": 610,
"preview": "# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.\n\n# Dependencies\nnode_modules\n.pnp\n"
},
{
"path": ".husky/commit-msg",
"chars": 32,
"preview": "pnpm exec commitlint --edit \"$1\""
},
{
"path": ".npmrc",
"chars": 31,
"preview": "public-hoist-pattern[]=*prisma*"
},
{
"path": ".vscode/extensions.json",
"chars": 137,
"preview": "{\n \"recommendations\": [\n \"biomejs.biome\",\n \"astro-build.astro-vscode\",\n \"bradlc.vscode-tailwindcss\",\n \"Pris"
},
{
"path": ".vscode/settings.json",
"chars": 1642,
"preview": "{\n \"tailwindCSS.experimental.configFile\": {\n \"./apps/web/src/styles/globals.css\": [\"./apps/web/src/**\"],\n \"./pack"
},
{
"path": "CODE_OF_CONDUCT.md",
"chars": 5222,
"preview": "# Contributor Covenant Code of Conduct\n\n## Our Pledge\n\nWe as members, contributors, and leaders pledge to make participa"
},
{
"path": "LICENSE",
"chars": 34523,
"preview": " GNU AFFERO GENERAL PUBLIC LICENSE\n Version 3, 19 November 2007\n\n Copyright (C)"
},
{
"path": "README.md",
"chars": 1245,
"preview": "<h1 align=\"center\">Marble</h1>\n\n<p align=\"center\">\n <a href=\"https://vercel.com/oss\">\n <img alt=\"Vercel OSS Program\""
},
{
"path": "apps/api/.gitignore",
"chars": 346,
"preview": "# prod\ndist/\n\n# dev\n.yarn/\n!.yarn/releases\n.vscode/*\n!.vscode/launch.json\n!.vscode/*.code-snippets\n.idea/workspace.xml\n."
},
{
"path": "apps/api/README.md",
"chars": 48,
"preview": "# API\n\nAPI endpoints users can fetch data from.\n"
},
{
"path": "apps/api/package.json",
"chars": 849,
"preview": "{\n \"name\": \"api\",\n \"version\": \"0.1.0\",\n \"scripts\": {\n \"dev\": \"wrangler dev\",\n \"deploy\": \"wrangler deploy --mini"
},
{
"path": "apps/api/src/app.ts",
"chars": 5565,
"preview": "import { OpenAPIHono } from \"@hono/zod-openapi\";\nimport { Hono } from \"hono\";\nimport { cors } from \"hono/cors\";\nimport {"
},
{
"path": "apps/api/src/index.ts",
"chars": 99,
"preview": "/** biome-ignore-all lint/performance/noBarrelFile: \"required\" */\nexport { default } from \"./app\";\n"
},
{
"path": "apps/api/src/lib/cache.ts",
"chars": 6334,
"preview": "import { Redis } from \"@upstash/redis/cloudflare\";\n\n/** Default cache TTL in seconds (1 hour) */\nconst DEFAULT_TTL = 360"
},
{
"path": "apps/api/src/lib/constants.ts",
"chars": 1658,
"preview": "/**\n * Available API route resources.\n * Used for route dispatching and redirect logic.\n */\nexport const ROUTES = [\n \"p"
},
{
"path": "apps/api/src/lib/crypto.ts",
"chars": 708,
"preview": "/**\n * Web Crypto API utilities for Cloudflare Workers\n * These functions use the native Web Crypto API instead of Node."
},
{
"path": "apps/api/src/lib/db.ts",
"chars": 1528,
"preview": "import { createClient as createHyperdriveClient } from \"@marble/db/hyperdrive\";\nimport { createClient as createWorkersCl"
},
{
"path": "apps/api/src/lib/events.ts",
"chars": 1160,
"preview": "import type { createDbClient } from \"@/lib/db\";\nimport type { JsonObject } from \"@/validations/json\";\nimport type {\n WO"
},
{
"path": "apps/api/src/lib/media.ts",
"chars": 2111,
"preview": "import { imageSize } from \"image-size\";\nimport { DEFAULT_CDN_URL } from \"./constants\";\n\nexport type MediaType = \"image\" "
},
{
"path": "apps/api/src/lib/polar.ts",
"chars": 218,
"preview": "import { Polar } from \"@polar-sh/sdk\";\n\nexport function createPolarClient(accessToken: string, isProduction = false) {\n "
},
{
"path": "apps/api/src/lib/posts.ts",
"chars": 1308,
"preview": "import { z } from \"@hono/zod-openapi\";\n\nexport const buildStatusFilter = (status: \"published\" | \"draft\" | \"all\") =>\n st"
},
{
"path": "apps/api/src/lib/redis.ts",
"chars": 161,
"preview": "import { Redis } from \"@upstash/redis/cloudflare\";\n\nexport function createRedisClient(url: string, token: string): Redis"
},
{
"path": "apps/api/src/lib/sanitize.ts",
"chars": 2970,
"preview": "import sanitize, { defaults } from \"sanitize-html\";\n\n/**\n * Sanitize HTML content to prevent XSS attacks.\n * Uses the sa"
},
{
"path": "apps/api/src/lib/usage.ts",
"chars": 8480,
"preview": "import { sendUsageLimitEmail } from \"@marble/email\";\nimport { getWorkspacePlan, PLAN_LIMITS, type PlanType } from \"@marb"
},
{
"path": "apps/api/src/lib/workspace.ts",
"chars": 1125,
"preview": "import type { Context } from \"hono\";\nimport { HTTPException } from \"hono/http-exception\";\n\n/**\n * Get the workspace ID f"
},
{
"path": "apps/api/src/middleware/analytics.ts",
"chars": 5024,
"preview": "import type { Context, MiddlewareHandler } from \"hono\";\nimport { createDbClient, type DbClient } from \"@/lib/db\";\nimport"
},
{
"path": "apps/api/src/middleware/authorization.ts",
"chars": 1272,
"preview": "import type { Context, MiddlewareHandler, Next } from \"hono\";\nimport { createDbClient, type DbClient } from \"@/lib/db\";\n"
},
{
"path": "apps/api/src/middleware/cache.ts",
"chars": 2293,
"preview": "import type { MiddlewareHandler } from \"hono\";\n\n/**\n * Default stale-if-error time in seconds.\n * This tells CDNs/browse"
},
{
"path": "apps/api/src/middleware/key-authorization.ts",
"chars": 2933,
"preview": "import type { MiddlewareHandler } from \"hono\";\nimport { hashApiKey } from \"@/lib/crypto\";\nimport { createDbClient, type "
},
{
"path": "apps/api/src/middleware/legacy-analytics.ts",
"chars": 2338,
"preview": "import type { Context, MiddlewareHandler, Next } from \"hono\";\nimport { createDbClient, type DbClient } from \"@/lib/db\";\n"
},
{
"path": "apps/api/src/middleware/ratelimit.ts",
"chars": 2317,
"preview": "import { Ratelimit } from \"@upstash/ratelimit\";\nimport type { Context, MiddlewareHandler, Next } from \"hono\";\nimport { c"
},
{
"path": "apps/api/src/middleware/system.ts",
"chars": 819,
"preview": "import type { MiddlewareHandler } from \"hono\";\nimport type { Env } from \"@/types/env\";\n\n/**\n * System Secret Authenticat"
},
{
"path": "apps/api/src/routes/authors.ts",
"chars": 18325,
"preview": "import { createRoute, OpenAPIHono, z } from \"@hono/zod-openapi\";\nimport { toAuthorPayload, withChanges } from \"@marble/e"
},
{
"path": "apps/api/src/routes/cache.ts",
"chars": 2075,
"preview": "import { Hono } from \"hono\";\nimport { createCacheClient } from \"@/lib/cache\";\nimport type { Env } from \"@/types/env\";\nim"
},
{
"path": "apps/api/src/routes/categories.ts",
"chars": 17008,
"preview": "import { createRoute, OpenAPIHono, z } from \"@hono/zod-openapi\";\nimport { toCategoryPayload, withChanges } from \"@marble"
},
{
"path": "apps/api/src/routes/events.ts",
"chars": 1861,
"preview": "import { Hono } from \"hono\";\nimport { createDbClient } from \"@/lib/db\";\nimport type { Env } from \"@/types/env\";\nimport {"
},
{
"path": "apps/api/src/routes/invalidate.ts",
"chars": 2587,
"preview": "import { Redis } from \"@upstash/redis/cloudflare\";\nimport { Hono } from \"hono\";\nimport { createCacheClient } from \"@/lib"
},
{
"path": "apps/api/src/routes/media.ts",
"chars": 14959,
"preview": "import { createRoute, OpenAPIHono } from \"@hono/zod-openapi\";\nimport { toMediaPayload, withChanges } from \"@marble/event"
},
{
"path": "apps/api/src/routes/posts.ts",
"chars": 29571,
"preview": "import { createRoute, OpenAPIHono, z } from \"@hono/zod-openapi\";\nimport { toPostPayload, withChanges } from \"@marble/eve"
},
{
"path": "apps/api/src/routes/tags.ts",
"chars": 15145,
"preview": "import { createRoute, OpenAPIHono, z } from \"@hono/zod-openapi\";\nimport { toTagPayload, withChanges } from \"@marble/even"
},
{
"path": "apps/api/src/schemas/authors.ts",
"chars": 4319,
"preview": "import { z } from \"@hono/zod-openapi\";\nimport { PaginationSchema } from \"./common\";\n\nexport const SocialSchema = z\n .ob"
},
{
"path": "apps/api/src/schemas/categories.ts",
"chars": 2306,
"preview": "import { z } from \"@hono/zod-openapi\";\nimport { PaginationSchema } from \"./common\";\n\nexport const CategorySchema = z\n ."
},
{
"path": "apps/api/src/schemas/common.ts",
"chars": 3316,
"preview": "import { z } from \"@hono/zod-openapi\";\n\nexport const PaginationSchema = z\n .object({\n limit: z.number().int().positi"
},
{
"path": "apps/api/src/schemas/media.ts",
"chars": 3229,
"preview": "import { z } from \"@hono/zod-openapi\";\nimport { LimitQuerySchema, PageQuerySchema, PaginationSchema } from \"./common\";\n\n"
},
{
"path": "apps/api/src/schemas/posts.ts",
"chars": 12351,
"preview": "import { z } from \"@hono/zod-openapi\";\nimport { ContentFormatSchema, PaginationSchema } from \"./common\";\n\nexport const S"
},
{
"path": "apps/api/src/schemas/tags.ts",
"chars": 2222,
"preview": "import { z } from \"@hono/zod-openapi\";\nimport { PaginationSchema } from \"./common\";\n\nexport const TagSchema = z\n .objec"
},
{
"path": "apps/api/src/types/env.ts",
"chars": 621,
"preview": "export interface Env {\n DATABASE_URL: string;\n HYPERDRIVE: { connectionString: string };\n STORAGE: R2Bucket;\n STORAG"
},
{
"path": "apps/api/src/validations/authors.ts",
"chars": 852,
"preview": "import { z } from \"zod\";\n\nexport const AuthorsQuerySchema = z.object({\n limit: z\n .string()\n .transform((val) => "
},
{
"path": "apps/api/src/validations/categories.ts",
"chars": 857,
"preview": "import { z } from \"zod\";\n\nexport const CategoriesQuerySchema = z.object({\n limit: z\n .string()\n .transform((val) "
},
{
"path": "apps/api/src/validations/json.ts",
"chars": 561,
"preview": "import { z } from \"zod\";\n\nconst JsonLiteralSchema = z.union([\n z.string(),\n z.number(),\n z.boolean(),\n z.null(),\n]);"
},
{
"path": "apps/api/src/validations/misc.ts",
"chars": 2043,
"preview": "import {\n WORKSPACE_EVENT_ACTOR_TYPES as EVENT_ACTOR_TYPES,\n WORKSPACE_EVENT_RESOURCE_TYPES as EVENT_RESOURCE_TYPES,\n "
},
{
"path": "apps/api/src/validations/posts.ts",
"chars": 1512,
"preview": "import { z } from \"zod\";\n\nexport const OrderSchema = z.enum([\"asc\", \"desc\"]).default(\"desc\");\n\nexport const PostsQuerySc"
},
{
"path": "apps/api/src/validations/tags.ts",
"chars": 846,
"preview": "import { z } from \"zod\";\n\nexport const TagsQuerySchema = z.object({\n limit: z\n .string()\n .transform((val) => {\n "
},
{
"path": "apps/api/tsconfig.json",
"chars": 317,
"preview": "{\n \"compilerOptions\": {\n \"target\": \"ESNext\",\n \"module\": \"ESNext\",\n \"moduleResolution\": \"Bundler\",\n \"strict\""
},
{
"path": "apps/api/wrangler.jsonc",
"chars": 945,
"preview": "{\n \"$schema\": \"node_modules/wrangler/config-schema.json\",\n \"name\": \"marble-api\",\n \"main\": \"src/index.ts\",\n \"compatib"
},
{
"path": "apps/cms/.env.example",
"chars": 921,
"preview": "# POSTGRES CONNECTION STRING\nDATABASE_URL=postgresql://usemarble:justusemarble@localhost:5432/marble\n\n# AUTH\nBETTER_AUTH"
},
{
"path": "apps/cms/.gitignore",
"chars": 494,
"preview": "# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.\n\n# dependencies\n/node_modules\n/.pn"
},
{
"path": "apps/cms/README.md",
"chars": 41,
"preview": "# CMS\n\nDashboard for content management.\n"
},
{
"path": "apps/cms/components.json",
"chars": 447,
"preview": "{\n \"$schema\": \"https://ui.shadcn.com/schema.json\",\n \"style\": \"default\",\n \"rsc\": true,\n \"tsx\": true,\n \"tailwind\": {\n"
},
{
"path": "apps/cms/next.config.ts",
"chars": 1147,
"preview": "import path from \"node:path\";\nimport type { NextConfig } from \"next\";\n\nconst nextConfig: NextConfig = {\n reactStrictMod"
},
{
"path": "apps/cms/package.json",
"chars": 2523,
"preview": "{\n \"name\": \"cms\",\n \"version\": \"0.1.0\",\n \"private\": true,\n \"scripts\": {\n \"dev\": \"next dev\",\n \"build\": \"next bui"
},
{
"path": "apps/cms/postcss.config.mjs",
"chars": 146,
"preview": "/** @type {import('postcss-load-config').Config} */\nconst config = {\n plugins: {\n \"@tailwindcss/postcss\": {},\n },\n}"
},
{
"path": "apps/cms/public/manifest.json",
"chars": 434,
"preview": "{\n \"name\": \"Marble\",\n \"short_name\": \"Marble\",\n \"icons\": [\n {\n \"src\": \"/web-app-manifest-192x192.png\",\n \""
},
{
"path": "apps/cms/src/app/(auth)/join/[id]/page-client.tsx",
"chars": 10102,
"preview": "\"use client\";\n\nimport {\n Avatar,\n AvatarFallback,\n AvatarImage,\n} from \"@marble/ui/components/avatar\";\nimport { butto"
},
{
"path": "apps/cms/src/app/(auth)/join/[id]/page.tsx",
"chars": 874,
"preview": "import { redirect } from \"next/navigation\";\nimport { Suspense } from \"react\";\nimport PageLoader from \"@/components/share"
},
{
"path": "apps/cms/src/app/(auth)/layout.tsx",
"chars": 149,
"preview": "export default function AuthLayout({\n children,\n}: {\n children: React.ReactNode;\n}) {\n return <div className=\"grid mi"
},
{
"path": "apps/cms/src/app/(auth)/login/page.tsx",
"chars": 3413,
"preview": "import { Separator } from \"@marble/ui/components/separator\";\nimport type { Metadata } from \"next\";\nimport Link from \"nex"
},
{
"path": "apps/cms/src/app/(auth)/new/page-client.tsx",
"chars": 6964,
"preview": "\"use client\";\n\nimport { zodResolver } from \"@hookform/resolvers/zod\";\nimport { buttonVariants } from \"@marble/ui/compone"
},
{
"path": "apps/cms/src/app/(auth)/new/page.tsx",
"chars": 302,
"preview": "import type { Metadata } from \"next\";\nimport { Suspense } from \"react\";\nimport PageClient from \"./page-client\";\n\nexport "
},
{
"path": "apps/cms/src/app/(auth)/register/page.tsx",
"chars": 2906,
"preview": "import type { Metadata } from \"next\";\nimport Link from \"next/link\";\nimport { Suspense } from \"react\";\nimport { RegisterF"
},
{
"path": "apps/cms/src/app/(auth)/reset/page.tsx",
"chars": 1130,
"preview": "import type { Metadata } from \"next\";\nimport { Suspense } from \"react\";\nimport { ResetForm } from \"@/components/auth/res"
},
{
"path": "apps/cms/src/app/(auth)/verify/page.tsx",
"chars": 797,
"preview": "import type { Metadata } from \"next\";\nimport { Suspense } from \"react\";\nimport { VerifyForm } from \"@/components/auth/ve"
},
{
"path": "apps/cms/src/app/(main)/[workspace]/(dashboard)/(home)/page-client.tsx",
"chars": 2136,
"preview": "\"use client\";\n\nimport { useQuery } from \"@tanstack/react-query\";\nimport { ApiUsageCard } from \"@/components/home/api-usa"
},
{
"path": "apps/cms/src/app/(main)/[workspace]/(dashboard)/(home)/page.tsx",
"chars": 210,
"preview": "import PageClient from \"./page-client\";\n\nexport const metadata = {\n title: \"Home\",\n description: \"Workspace overview a"
},
{
"path": "apps/cms/src/app/(main)/[workspace]/(dashboard)/authors/page-client.tsx",
"chars": 1636,
"preview": "\"use client\";\n\nimport { useQuery } from \"@tanstack/react-query\";\nimport { toast } from \"sonner\";\nimport { columns } from"
},
{
"path": "apps/cms/src/app/(main)/[workspace]/(dashboard)/authors/page.tsx",
"chars": 250,
"preview": "import type { Metadata } from \"next\";\nimport PageClient from \"./page-client\";\n\nexport const metadata: Metadata = {\n tit"
},
{
"path": "apps/cms/src/app/(main)/[workspace]/(dashboard)/categories/page-client.tsx",
"chars": 3154,
"preview": "\"use client\";\n\nimport { Package01Icon } from \"@hugeicons/core-free-icons\";\nimport { HugeiconsIcon } from \"@hugeicons/rea"
},
{
"path": "apps/cms/src/app/(main)/[workspace]/(dashboard)/categories/page.tsx",
"chars": 202,
"preview": "import PageClient from \"./page-client\";\n\nexport const metadata = {\n title: \"Categories\",\n description: \"Manage your ca"
},
{
"path": "apps/cms/src/app/(main)/[workspace]/(dashboard)/layout.tsx",
"chars": 1039,
"preview": "import { SidebarInset, SidebarProvider } from \"@marble/ui/components/sidebar\";\nimport { AppSidebar } from \"@/components/"
},
{
"path": "apps/cms/src/app/(main)/[workspace]/(dashboard)/loading.tsx",
"chars": 122,
"preview": "import PageLoader from \"@/components/shared/page-loader\";\n\nexport default function Loading() {\n return <PageLoader />;\n"
},
{
"path": "apps/cms/src/app/(main)/[workspace]/(dashboard)/media/[id]/page-client.tsx",
"chars": 10963,
"preview": "\"use client\";\n\nimport {\n ArrowLeft02Icon,\n Copy01Icon,\n Download01Icon,\n} from \"@hugeicons/core-free-icons\";\nimport {"
},
{
"path": "apps/cms/src/app/(main)/[workspace]/(dashboard)/media/[id]/page.tsx",
"chars": 344,
"preview": "import MediaDetailPage from \"./page-client\";\n\nexport const metadata = {\n title: \"Media\",\n description: \"Manage your me"
},
{
"path": "apps/cms/src/app/(main)/[workspace]/(dashboard)/media/page-client.tsx",
"chars": 5390,
"preview": "\"use client\";\n\nimport { toast } from \"@marble/ui/components/sonner\";\nimport { keepPreviousData, useQuery } from \"@tansta"
},
{
"path": "apps/cms/src/app/(main)/[workspace]/(dashboard)/media/page.tsx",
"chars": 192,
"preview": "import PageClient from \"./page-client\";\n\nexport const metadata = {\n title: \"Media\",\n description: \"Manage your media\","
},
{
"path": "apps/cms/src/app/(main)/[workspace]/(dashboard)/posts/page-client.tsx",
"chars": 4801,
"preview": "\"use client\";\n\nimport { Files01Icon } from \"@hugeicons/core-free-icons\";\nimport { HugeiconsIcon } from \"@hugeicons/react"
},
{
"path": "apps/cms/src/app/(main)/[workspace]/(dashboard)/posts/page.tsx",
"chars": 192,
"preview": "import PageClient from \"./page-client\";\n\nexport const metadata = {\n title: \"Posts\",\n description: \"Manage your posts\","
},
{
"path": "apps/cms/src/app/(main)/[workspace]/(dashboard)/settings/account/page-client.tsx",
"chars": 7953,
"preview": "\"use client\";\n\nimport { zodResolver } from \"@hookform/resolvers/zod\";\nimport { Alert02Icon } from \"@hugeicons/core-free-"
},
{
"path": "apps/cms/src/app/(main)/[workspace]/(dashboard)/settings/account/page.tsx",
"chars": 259,
"preview": "import type { Metadata } from \"next\";\nimport PageClient from \"./page-client\";\n\nexport const metadata: Metadata = {\n tit"
},
{
"path": "apps/cms/src/app/(main)/[workspace]/(dashboard)/settings/appearance/page-client.tsx",
"chars": 1085,
"preview": "\"use client\";\n\nimport { DesktopIcon } from \"@phosphor-icons/react\";\nimport { DashboardBody } from \"@/components/layout/w"
},
{
"path": "apps/cms/src/app/(main)/[workspace]/(dashboard)/settings/appearance/page.tsx",
"chars": 268,
"preview": "import type { Metadata } from \"next\";\nimport PageClient from \"./page-client\";\n\nexport const metadata: Metadata = {\n tit"
},
{
"path": "apps/cms/src/app/(main)/[workspace]/(dashboard)/settings/billing/page-client.tsx",
"chars": 9131,
"preview": "\"use client\";\n\nimport { Badge } from \"@marble/ui/components/badge\";\nimport { Button } from \"@marble/ui/components/button"
},
{
"path": "apps/cms/src/app/(main)/[workspace]/(dashboard)/settings/billing/page.tsx",
"chars": 269,
"preview": "import type { Metadata } from \"next\";\nimport PageClient from \"./page-client\";\n\nexport const metadata: Metadata = {\n tit"
},
{
"path": "apps/cms/src/app/(main)/[workspace]/(dashboard)/settings/fields/page-client.tsx",
"chars": 5534,
"preview": "\"use client\";\n\nimport { DatabaseIcon } from \"@hugeicons/core-free-icons\";\nimport { HugeiconsIcon } from \"@hugeicons/reac"
},
{
"path": "apps/cms/src/app/(main)/[workspace]/(dashboard)/settings/fields/page.tsx",
"chars": 241,
"preview": "import { PageClient } from \"./page-client\";\n\nexport const metadata = {\n title: \"Custom Fields\",\n description: \"Define "
},
{
"path": "apps/cms/src/app/(main)/[workspace]/(dashboard)/settings/general/page-client.tsx",
"chars": 952,
"preview": "\"use client\";\n\nimport { DashboardBody } from \"@/components/layout/wrapper\";\nimport { Delete } from \"@/components/setting"
},
{
"path": "apps/cms/src/app/(main)/[workspace]/(dashboard)/settings/general/page.tsx",
"chars": 269,
"preview": "import type { Metadata } from \"next\";\nimport PageClient from \"./page-client\";\n\nexport const metadata: Metadata = {\n tit"
},
{
"path": "apps/cms/src/app/(main)/[workspace]/(dashboard)/settings/keys/page-client.tsx",
"chars": 3976,
"preview": "\"use client\";\n\nimport { Key01Icon, PlusSignIcon } from \"@hugeicons/core-free-icons\";\nimport { HugeiconsIcon } from \"@hug"
},
{
"path": "apps/cms/src/app/(main)/[workspace]/(dashboard)/settings/keys/page.tsx",
"chars": 198,
"preview": "import PageClient from \"./page-client\";\n\nexport const metadata = {\n title: \"API Keys\",\n description: \"Manage your API "
},
{
"path": "apps/cms/src/app/(main)/[workspace]/(dashboard)/settings/members/page-client.tsx",
"chars": 2373,
"preview": "\"use client\";\n\nimport dynamic from \"next/dynamic\";\nimport { useState } from \"react\";\nimport { DashboardBody } from \"@/co"
},
{
"path": "apps/cms/src/app/(main)/[workspace]/(dashboard)/settings/members/page.tsx",
"chars": 272,
"preview": "import type { Metadata } from \"next\";\nimport PageClient from \"./page-client\";\n\nexport const metadata: Metadata = {\n tit"
},
{
"path": "apps/cms/src/app/(main)/[workspace]/(dashboard)/settings/notifications/page-client.tsx",
"chars": 5852,
"preview": "\"use client\";\n\nimport { toast } from \"@marble/ui/components/sonner\";\nimport { Switch } from \"@marble/ui/components/switc"
},
{
"path": "apps/cms/src/app/(main)/[workspace]/(dashboard)/settings/notifications/page.tsx",
"chars": 273,
"preview": "import type { Metadata } from \"next\";\nimport PageClient from \"./page-client\";\n\nexport const metadata: Metadata = {\n tit"
},
{
"path": "apps/cms/src/app/(main)/[workspace]/(dashboard)/settings/webhooks/page-client.tsx",
"chars": 3780,
"preview": "\"use client\";\n\nimport { toast } from \"@marble/ui/components/sonner\";\nimport { useMutation, useQuery, useQueryClient } fr"
},
{
"path": "apps/cms/src/app/(main)/[workspace]/(dashboard)/settings/webhooks/page.tsx",
"chars": 242,
"preview": "import { PageClient } from \"./page-client\";\n\nexport const metadata = {\n title: \"Webhooks\",\n description: \"Create webho"
},
{
"path": "apps/cms/src/app/(main)/[workspace]/(dashboard)/tags/page-client.tsx",
"chars": 2993,
"preview": "\"use client\";\n\nimport { Tag01Icon } from \"@hugeicons/core-free-icons\";\nimport { HugeiconsIcon } from \"@hugeicons/react\";"
},
{
"path": "apps/cms/src/app/(main)/[workspace]/(dashboard)/tags/page.tsx",
"chars": 190,
"preview": "import PageClient from \"./page-client\";\n\nexport const metadata = {\n title: \"Tags\",\n description: \"Manage your tags\",\n}"
},
{
"path": "apps/cms/src/app/(main)/[workspace]/(editor)/editor/p/[id]/page-client.tsx",
"chars": 406,
"preview": "\"use client\";\n\nimport { useParams } from \"next/navigation\";\nimport { EditorDataProvider } from \"@/components/editor/edit"
},
{
"path": "apps/cms/src/app/(main)/[workspace]/(editor)/editor/p/[id]/page.tsx",
"chars": 219,
"preview": "import type { Metadata } from \"next\";\nimport PageClient from \"./page-client\";\n\nexport const metadata: Metadata = {\n tit"
},
{
"path": "apps/cms/src/app/(main)/[workspace]/(editor)/editor/p/new/page-client.tsx",
"chars": 309,
"preview": "\"use client\";\n\nimport { EditorDataProvider } from \"@/components/editor/editor-data-provider\";\nimport EditorPage from \"@/"
},
{
"path": "apps/cms/src/app/(main)/[workspace]/(editor)/editor/p/new/page.tsx",
"chars": 223,
"preview": "import type { Metadata } from \"next\";\nimport NewPostPageClient from \"./page-client\";\n\nexport const metadata: Metadata = "
},
{
"path": "apps/cms/src/app/(main)/[workspace]/(editor)/layout.tsx",
"chars": 591,
"preview": "import { SidebarProvider } from \"@marble/ui/components/sidebar\";\n\nfunction EditorLayout({ children }: { children: React."
},
{
"path": "apps/cms/src/app/(main)/[workspace]/layout.tsx",
"chars": 1072,
"preview": "// app/(main)/[workspace]/layout.tsx\nimport { notFound } from \"next/navigation\";\nimport { setActiveWorkspace } from \"@/l"
},
{
"path": "apps/cms/src/app/(main)/[workspace]/loading.tsx",
"chars": 185,
"preview": "import PageLoader from \"@/components/shared/page-loader\";\n\nexport default function Loading() {\n return (\n <div class"
},
{
"path": "apps/cms/src/app/(main)/[workspace]/set-workspace-cookie.tsx",
"chars": 325,
"preview": "\"use client\";\n\nimport { useEffect } from \"react\";\nimport { setServerLastVisitedWorkspace } from \"@/utils/workspace/serve"
},
{
"path": "apps/cms/src/app/(main)/layout.tsx",
"chars": 248,
"preview": "import { UserProvider } from \"@/providers/user\";\n\nexport default async function MainLayout({\n children,\n}: {\n children"
},
{
"path": "apps/cms/src/app/(share)/layout.tsx",
"chars": 150,
"preview": "export default function ShareLayout({\n children,\n}: {\n children: React.ReactNode;\n}) {\n return <div className=\"grid m"
},
{
"path": "apps/cms/src/app/(share)/share/[token]/page-client.tsx",
"chars": 3228,
"preview": "\"use client\";\n\nimport {\n Avatar,\n AvatarFallback,\n AvatarImage,\n} from \"@marble/ui/components/avatar\";\nimport { forma"
},
{
"path": "apps/cms/src/app/(share)/share/[token]/page.tsx",
"chars": 1471,
"preview": "import { highlightContent } from \"@marble/utils\";\nimport type { Metadata } from \"next\";\nimport { notFound } from \"next/n"
},
{
"path": "apps/cms/src/app/api/accounts/[id]/route.ts",
"chars": 573,
"preview": "import { db } from \"@marble/db\";\nimport { NextResponse } from \"next/server\";\nimport { getServerSession } from \"@/lib/aut"
},
{
"path": "apps/cms/src/app/api/accounts/route.ts",
"chars": 1161,
"preview": "import { db } from \"@marble/db\";\nimport { NextResponse } from \"next/server\";\nimport { getServerSession } from \"@/lib/aut"
},
{
"path": "apps/cms/src/app/api/ai/suggestions/prompt.ts",
"chars": 3677,
"preview": "export interface SystemPromptParams {\n metrics: {\n wordCount: number;\n sentenceCount: number;\n wordsPerSentenc"
},
{
"path": "apps/cms/src/app/api/ai/suggestions/route.tsx",
"chars": 3597,
"preview": "import { createHash } from \"node:crypto\";\nimport { db } from \"@marble/db\";\nimport { NextResponse } from \"next/server\";\ni"
},
{
"path": "apps/cms/src/app/api/auth/[...all]/route.ts",
"chars": 150,
"preview": "import { toNextJsHandler } from \"better-auth/next-js\";\nimport { auth } from \"@/lib/auth/server\";\n\nexport const { POST, G"
},
{
"path": "apps/cms/src/app/api/authors/[id]/route.ts",
"chars": 4574,
"preview": "import { db } from \"@marble/db\";\nimport { toAuthorPayload, withChanges } from \"@marble/events\";\nimport { NextResponse } "
},
{
"path": "apps/cms/src/app/api/authors/route.ts",
"chars": 5152,
"preview": "import { db } from \"@marble/db\";\nimport { toAuthorPayload } from \"@marble/events\";\nimport { NextResponse } from \"next/se"
},
{
"path": "apps/cms/src/app/api/categories/[id]/route.ts",
"chars": 3586,
"preview": "import { db } from \"@marble/db\";\nimport { toCategoryPayload, withChanges } from \"@marble/events\";\nimport { NextResponse "
},
{
"path": "apps/cms/src/app/api/categories/route.ts",
"chars": 2484,
"preview": "import { db } from \"@marble/db\";\nimport { toCategoryPayload } from \"@marble/events\";\nimport { NextResponse } from \"next/"
},
{
"path": "apps/cms/src/app/api/fields/[id]/route.ts",
"chars": 6927,
"preview": "import { db } from \"@marble/db\";\nimport { Prisma } from \"@marble/db/browser\";\nimport { NextResponse } from \"next/server\""
},
{
"path": "apps/cms/src/app/api/fields/route.ts",
"chars": 3530,
"preview": "import { db } from \"@marble/db\";\nimport type { FieldType as PrismaFieldType } from \"@marble/db/browser\";\nimport { NextRe"
},
{
"path": "apps/cms/src/app/api/keys/[id]/route.ts",
"chars": 3810,
"preview": "import { db } from \"@marble/db\";\nimport { NextResponse } from \"next/server\";\nimport { requireActiveWorkspaceAccess } fro"
},
{
"path": "apps/cms/src/app/api/keys/route.ts",
"chars": 2427,
"preview": "import { db } from \"@marble/db\";\nimport { generateApiKey } from \"@marble/utils\";\nimport { NextResponse } from \"next/serv"
},
{
"path": "apps/cms/src/app/api/media/[id]/route.ts",
"chars": 3084,
"preview": "import { db } from \"@marble/db\";\nimport { NextResponse } from \"next/server\";\nimport { z } from \"zod\";\nimport { requireAc"
},
{
"path": "apps/cms/src/app/api/media/editor/route.ts",
"chars": 3503,
"preview": "import { db } from \"@marble/db\";\nimport { NextResponse } from \"next/server\";\nimport { z } from \"zod\";\nimport { requireAc"
},
{
"path": "apps/cms/src/app/api/media/route.ts",
"chars": 6336,
"preview": "import { DeleteObjectCommand } from \"@aws-sdk/client-s3\";\nimport { db } from \"@marble/db\";\nimport { toMediaPayload } fro"
},
{
"path": "apps/cms/src/app/api/metrics/publishing/route.ts",
"chars": 1901,
"preview": "import { db } from \"@marble/db\";\nimport { eachDayOfInterval, endOfYear, format, startOfYear } from \"date-fns\";\nimport { "
},
{
"path": "apps/cms/src/app/api/metrics/usage/route.ts",
"chars": 6121,
"preview": "import { db } from \"@marble/db\";\nimport { UsageEventType } from \"@marble/db/browser\";\nimport { addDays, format, startOfD"
},
{
"path": "apps/cms/src/app/api/polar/success/route.ts",
"chars": 1474,
"preview": "import { db } from \"@marble/db\";\nimport { cookies } from \"next/headers\";\nimport { NextResponse } from \"next/server\";\nimp"
},
{
"path": "apps/cms/src/app/api/posts/[id]/fields/route.ts",
"chars": 3744,
"preview": "import { db } from \"@marble/db\";\nimport { NextResponse } from \"next/server\";\nimport { requireActiveWorkspaceAccess } fro"
},
{
"path": "apps/cms/src/app/api/posts/[id]/route.ts",
"chars": 9030,
"preview": "import { db } from \"@marble/db\";\nimport { toPostPayload, withChanges } from \"@marble/events\";\nimport { NextResponse } fr"
},
{
"path": "apps/cms/src/app/api/posts/import/route.ts",
"chars": 3994,
"preview": "import { db } from \"@marble/db\";\nimport { toPostPayload } from \"@marble/events\";\nimport { markdownToHtml, markdownToTipt"
},
{
"path": "apps/cms/src/app/api/posts/route.ts",
"chars": 10754,
"preview": "import { db } from \"@marble/db\";\nimport { toPostPayload } from \"@marble/events\";\nimport { nanoid } from \"nanoid\";\nimport"
},
{
"path": "apps/cms/src/app/api/share/[token]/route.ts",
"chars": 1853,
"preview": "import { db } from \"@marble/db\";\nimport { NextResponse } from \"next/server\";\n\nconst NO_STORE_HEADERS = {\n \"Cache-Contro"
},
{
"path": "apps/cms/src/app/api/share/route.ts",
"chars": 2066,
"preview": "import { db } from \"@marble/db\";\nimport { nanoid } from \"nanoid\";\nimport { NextResponse } from \"next/server\";\nimport { c"
},
{
"path": "apps/cms/src/app/api/tags/[id]/route.ts",
"chars": 3419,
"preview": "import { db } from \"@marble/db\";\nimport { toTagPayload, withChanges } from \"@marble/events\";\nimport { NextResponse } fro"
},
{
"path": "apps/cms/src/app/api/tags/route.ts",
"chars": 2177,
"preview": "import { db } from \"@marble/db\";\nimport { toTagPayload } from \"@marble/events\";\nimport { NextResponse } from \"next/serve"
},
{
"path": "apps/cms/src/app/api/upload/complete/route.ts",
"chars": 3220,
"preview": "import { db } from \"@marble/db\";\nimport { toMediaPayload } from \"@marble/events\";\nimport { NextResponse } from \"next/ser"
},
{
"path": "apps/cms/src/app/api/upload/route.ts",
"chars": 2347,
"preview": "import { PutObjectCommand } from \"@aws-sdk/client-s3\";\nimport { getSignedUrl } from \"@aws-sdk/s3-request-presigner\";\nimp"
},
{
"path": "apps/cms/src/app/api/user/notifications/route.ts",
"chars": 3893,
"preview": "import { db } from \"@marble/db\";\nimport { NextResponse } from \"next/server\";\nimport { requireActiveWorkspaceAccess } fro"
},
{
"path": "apps/cms/src/app/api/user/route.ts",
"chars": 2945,
"preview": "import { db } from \"@marble/db\";\nimport { NextResponse } from \"next/server\";\nimport { requireActiveWorkspaceAccess } fro"
},
{
"path": "apps/cms/src/app/api/webhooks/[id]/route.ts",
"chars": 2918,
"preview": "import { db } from \"@marble/db\";\nimport { NextResponse } from \"next/server\";\nimport { requireActiveWorkspaceAccess } fro"
},
{
"path": "apps/cms/src/app/api/webhooks/[id]/test/route.ts",
"chars": 1807,
"preview": "import { getDemoPostPublishedPayload } from \"@marble/events\";\nimport { NextResponse } from \"next/server\";\nimport { requi"
},
{
"path": "apps/cms/src/app/api/webhooks/route.ts",
"chars": 1242,
"preview": "import { randomBytes } from \"node:crypto\";\nimport { db } from \"@marble/db\";\nimport { NextResponse } from \"next/server\";\n"
},
{
"path": "apps/cms/src/app/api/workspaces/[slug]/route.ts",
"chars": 2804,
"preview": "import { db } from \"@marble/db\";\nimport { NextResponse } from \"next/server\";\nimport { getServerSession } from \"@/lib/aut"
},
{
"path": "apps/cms/src/app/api/workspaces/route.ts",
"chars": 2411,
"preview": "import { db } from \"@marble/db\";\nimport { NextResponse } from \"next/server\";\nimport { getServerSession } from \"@/lib/aut"
},
{
"path": "apps/cms/src/app/layout.tsx",
"chars": 1933,
"preview": "import type { Metadata } from \"next\";\nimport \"@/styles/globals.css\";\nimport { Databuddy } from \"@databuddy/sdk/react\";\ni"
},
{
"path": "apps/cms/src/app/not-found.tsx",
"chars": 573,
"preview": "\"use client\";\n\nimport { Button } from \"@marble/ui/components/button\";\nimport { ArrowArcLeftIcon } from \"@phosphor-icons/"
},
{
"path": "apps/cms/src/app/providers.tsx",
"chars": 1035,
"preview": "\"use client\";\n\nimport { Toaster } from \"@marble/ui/components/sonner\";\nimport { TooltipProvider } from \"@marble/ui/compo"
},
{
"path": "apps/cms/src/app/robots.ts",
"chars": 263,
"preview": "import type { MetadataRoute } from \"next\";\n\nexport default function robots(): MetadataRoute.Robots {\n return {\n rule"
},
{
"path": "apps/cms/src/components/auth/login-form.tsx",
"chars": 7087,
"preview": "\"use client\";\n\nimport { zodResolver } from \"@hookform/resolvers/zod\";\nimport { Input } from \"@marble/ui/components/input"
},
{
"path": "apps/cms/src/components/auth/register-form.tsx",
"chars": 7326,
"preview": "\"use client\";\n\nimport { zodResolver } from \"@hookform/resolvers/zod\";\nimport { Input } from \"@marble/ui/components/input"
},
{
"path": "apps/cms/src/components/auth/reset/reset-form.tsx",
"chars": 2725,
"preview": "\"use client\";\n\nimport { Input } from \"@marble/ui/components/input\";\nimport { toast } from \"@marble/ui/components/sonner\""
},
{
"path": "apps/cms/src/components/auth/reset/reset-request-form.tsx",
"chars": 2887,
"preview": "\"use client\";\n\nimport { Input } from \"@marble/ui/components/input\";\nimport { Label } from \"@marble/ui/components/label\";"
},
{
"path": "apps/cms/src/components/auth/verify-form.tsx",
"chars": 4552,
"preview": "\"use client\";\n\nimport {\n InputOTP,\n InputOTPGroup,\n InputOTPSlot,\n} from \"@marble/ui/components/input-otp\";\nimport { "
},
{
"path": "apps/cms/src/components/authors/author-modals.tsx",
"chars": 3230,
"preview": "/** biome-ignore-all lint/correctness/useUniqueElementIds: IDs are unique within their respective modals */\n\"use client\""
},
{
"path": "apps/cms/src/components/authors/author-sheet.tsx",
"chars": 14887,
"preview": "/** biome-ignore-all lint/correctness/useUniqueElementIds: IDs are unique within their respective modals */\n\"use client\""
},
{
"path": "apps/cms/src/components/authors/columns.tsx",
"chars": 1665,
"preview": "\"use client\";\n\nimport {\n Avatar,\n AvatarFallback,\n AvatarImage,\n} from \"@marble/ui/components/avatar\";\nimport { Badge"
},
{
"path": "apps/cms/src/components/authors/data-table.tsx",
"chars": 5264,
"preview": "\"use client\";\n\nimport { Users } from \"@hugeicons/core-free-icons\";\nimport { HugeiconsIcon } from \"@hugeicons/react\";\nimp"
},
{
"path": "apps/cms/src/components/authors/table-actions.tsx",
"chars": 2156,
"preview": "\"use client\";\n\nimport {\n Delete02Icon,\n MoreVerticalIcon,\n PencilEdit02Icon,\n} from \"@hugeicons/core-free-icons\";\nimp"
},
{
"path": "apps/cms/src/components/billing/success-modal.tsx",
"chars": 3680,
"preview": "\"use client\";\n\nimport { Button } from \"@marble/ui/components/button\";\nimport {\n Dialog,\n DialogContent,\n DialogDescri"
},
{
"path": "apps/cms/src/components/billing/upgrade-modal.tsx",
"chars": 4021,
"preview": "\"use client\";\n\nimport { ShoppingCart02Icon } from \"@hugeicons/core-free-icons\";\nimport { HugeiconsIcon } from \"@hugeicon"
},
{
"path": "apps/cms/src/components/categories/category-modals.tsx",
"chars": 9878,
"preview": "\"use client\";\n\nimport { zodResolver } from \"@hookform/resolvers/zod\";\nimport { Alert02Icon, Package01Icon } from \"@hugei"
},
{
"path": "apps/cms/src/components/categories/columns.tsx",
"chars": 903,
"preview": "\"use client\";\n\nimport type { ColumnDef } from \"@tanstack/react-table\";\nimport TableActions from \"./table-actions\";\n\nexpo"
},
{
"path": "apps/cms/src/components/categories/data-table.tsx",
"chars": 4233,
"preview": "\"use client\";\n\nimport { Button } from \"@marble/ui/components/button\";\nimport { Input } from \"@marble/ui/components/input"
},
{
"path": "apps/cms/src/components/categories/table-actions.tsx",
"chars": 1981,
"preview": "import {\n Delete02Icon,\n MoreVerticalIcon,\n PencilEdit02Icon,\n} from \"@hugeicons/core-free-icons\";\nimport { Hugeicons"
},
{
"path": "apps/cms/src/components/editor/ai/readability-suggestions.tsx",
"chars": 4045,
"preview": "\"use client\";\n\nimport type { Editor } from \"@marble/editor\";\nimport { cn } from \"@marble/ui/lib/utils\";\nimport { CursorC"
},
{
"path": "apps/cms/src/components/editor/editor-data-provider.tsx",
"chars": 9570,
"preview": "\"use client\";\n\nimport { zodResolver } from \"@hookform/resolvers/zod\";\nimport { toast } from \"@marble/ui/components/sonne"
},
{
"path": "apps/cms/src/components/editor/editor-header.tsx",
"chars": 2119,
"preview": "\"use client\";\n\nimport {\n Cancel01Icon,\n SidebarRight01Icon,\n SidebarRightIcon,\n} from \"@hugeicons/core-free-icons\";\ni"
},
{
"path": "apps/cms/src/components/editor/editor-page.tsx",
"chars": 7878,
"preview": "\"use client\";\n\nimport { MarbleEditorMenus } from \"@/components/editor/editor\";\nimport { useEditorData } from \"@/componen"
},
{
"path": "apps/cms/src/components/editor/editor-sidebar.tsx",
"chars": 7277,
"preview": "\"use client\";\n\nimport { useCurrentEditor } from \"@marble/editor\";\nimport {\n Sidebar,\n SidebarContent,\n SidebarFooter,"
},
{
"path": "apps/cms/src/components/editor/editor.tsx",
"chars": 1789,
"preview": "\"use client\";\n\nimport {\n EditorAlignSelector,\n EditorBlockHandleMenu,\n EditorBubbleMenu,\n EditorClearFormatting,\n E"
},
{
"path": "apps/cms/src/components/editor/fields/author-selector.tsx",
"chars": 8871,
"preview": "\"use client\";\n\nimport {\n Avatar,\n AvatarFallback,\n AvatarImage,\n} from \"@marble/ui/components/avatar\";\nimport {\n Com"
},
{
"path": "apps/cms/src/components/editor/fields/category-selector.tsx",
"chars": 4363,
"preview": "import { Label } from \"@marble/ui/components/label\";\nimport {\n Select,\n SelectContent,\n SelectGroup,\n SelectItem,\n "
},
{
"path": "apps/cms/src/components/editor/fields/cover-image-selector.tsx",
"chars": 13985,
"preview": "\"use client\";\n\nimport { Album02Icon } from \"@hugeicons/core-free-icons\";\nimport { HugeiconsIcon } from \"@hugeicons/react"
},
{
"path": "apps/cms/src/components/editor/fields/custom-fields-section.tsx",
"chars": 13347,
"preview": "\"use client\";\n\nimport { FieldRichTextEditor } from \"@marble/editor\";\nimport { Badge } from \"@marble/ui/components/badge\""
},
{
"path": "apps/cms/src/components/editor/fields/description-field.tsx",
"chars": 1261,
"preview": "\"use client\";\n\nimport { Label } from \"@marble/ui/components/label\";\nimport { Textarea } from \"@marble/ui/components/text"
},
{
"path": "apps/cms/src/components/editor/fields/featured-field.tsx",
"chars": 1011,
"preview": "\"use client\";\n\nimport { Label } from \"@marble/ui/components/label\";\nimport { Switch } from \"@marble/ui/components/switch"
},
{
"path": "apps/cms/src/components/editor/fields/field-info.tsx",
"chars": 652,
"preview": "import {\n Tooltip,\n TooltipContent,\n TooltipTrigger,\n} from \"@marble/ui/components/tooltip\";\nimport { InfoIcon } from"
},
{
"path": "apps/cms/src/components/editor/fields/publish-date-field.tsx",
"chars": 2739,
"preview": "\"use client\";\n\nimport { Button } from \"@marble/ui/components/button\";\nimport { Calendar } from \"@marble/ui/components/ca"
},
{
"path": "apps/cms/src/components/editor/fields/slug-field.tsx",
"chars": 1441,
"preview": "\"use client\";\n\nimport { Input } from \"@marble/ui/components/input\";\nimport { Label } from \"@marble/ui/components/label\";"
},
{
"path": "apps/cms/src/components/editor/fields/status-field.tsx",
"chars": 1079,
"preview": "\"use client\";\n\nimport { Label } from \"@marble/ui/components/label\";\nimport { Switch } from \"@marble/ui/components/switch"
}
]
// ... and 530 more files (download for full content)
About this extraction
This page contains the full source code of the usemarble/marble GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 730 files (2.3 MB), approximately 656.8k tokens, and a symbol index with 1484 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.
Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.