Platform Feature List
A reference of every feature the ReadyEditor SaaS platform provides, grouped by area.
Plans
Four active plans plus a placeholder for future custom contracts.
| Plan key | Label | Price (monthly) | Price (annual/mo) |
|---|---|---|---|
dev |
Dev | Free | Free |
basic |
Starter | $19 | $15 |
pro |
Pro | $59 | $47 |
agency |
Agency | $129 | $99 |
Per-plan limits
| Dev | Starter | Pro | Agency | |
|---|---|---|---|---|
| Projects | 1 | 3 | 10 | Unlimited |
| Domains per project | 3 | Unlimited | Unlimited | Unlimited |
| Team members | 1 | 2 | 5 | 15 |
| Plugins | 5 core | 14 standard | All 35 | All 35 |
| Hide "Powered by ReadyEditor" badge | No | Yes | Yes | Yes |
| Usage analytics dashboard | No | No | Yes | Yes |
Limits are enforced server-side: creating a project, adding a domain, or inviting a team member beyond the plan limit returns a validation error.
Editor delivery
- Versioned releases — editor assets (JS, CSS, plugins) are published to
public/releases/{version}/with amanifest.jsonthat includes SRI integrity hashes for every file. - Loader-based install — a single
<script>tag withdata-api-keybootstraps the full editor: calls/api/v1/editor/init, fetches entitled asset URLs from the manifest, and initializes all enabled plugins. - JS API install — alternative to the loader; call
ReadyEditorLoader.init({...})manually from your own bundle after loading the loader script. - Legacy multi-tag snippet — supported for backwards compatibility.
- SRI enforcement — loader validates integrity hashes before executing plugin scripts; disable per-project for local dev via
data-require-sri="0"or the dashboard toggle. - CDN delivery — asset base URL is configurable via
EDITOR_RELEASE_BASE_URL(e.g. a CDN origin); falls back to the app origin. - Per-workspace release pinning — workspace owners/admins can select a specific editor version from the last 3 published releases; stored as a
tenant_overridesrow, takes precedence over the global default. - Badge control — the "Powered by ReadyEditor" status-bar link is shown/hidden based on plan: Dev always shows it, all paid plans suppress it. Controlled via
hideBadgein the init response, read by the editor UI at runtime.
Init endpoint (GET /api/v1/editor/init)
The central gating endpoint. Called by the loader on every page load.
Validates:
- API key exists and is active
- Project is active
- Origin/domain is in the project's allowed domains list
- Workspace subscription is not canceled, unpaid, or blocked
Returns on success:
{
"ok": true,
"plan": "pro",
"features": ["clipboard", "formatting_basic", "..."],
"hideBadge": true,
"domain": "example.com",
"language": "en",
"releaseVersion": "0.0.45",
"releaseBase": "https://cdn.example.com/releases/0.0.45",
"assets": {
"css": [{ "path": "...", "url": "...", "integrity": "sha384-..." }],
"js": {
"core": [...],
"plugins": [{ "name": "clipboard", "path": "...", "url": "...", "integrity": "..." }]
}
},
"sdkVersion": "...",
"ts": 1234567890
}
Error codes: missing_api_key, invalid_api_key, project_inactive, domain_not_allowed, subscription_blocked.
Caching: s-maxage=60, stale-while-revalidate=300 — CDN-friendly. ETag / 304 support for conditional requests.
Plugin system
- 35 registered plugins — see plugins.md for the full catalog.
- Plan-based entitlements —
plugin_entitlementstable maps plan → plugin names; resolved byEntitlementsResolver. - Workspace-level overrides — admin can grant or revoke individual plugins for a workspace via
entitlement_overrides(e.g. early access to a plugin not yet in a plan). - Project-level toggles — within entitled plugins, workspace owners/admins can disable individual plugins per project via the project settings UI. Stored in
project_plugin_settings. - Toolbar presets —
data-toolbar-preset="full|basic|minimal"controls which toolbar groups render without changing the entitlement set. See toolbar-presets.md.
Projects
- Create, list, and view projects scoped to a workspace.
- Each project has a unique slug, an active/inactive flag, and its own API key(s), domain allowlist, and plugin settings.
- Project count enforced against the workspace's plan limit on creation.
- Per-project usage stats available on the project detail page (all plans).
API keys
- Multiple API keys per project; each has a
name,public_key, andis_activeflag. - Keys are created and revoked from the project settings page.
last_used_atis updated on each successful init call.- Only workspace owners/admins (
canManageProjects) can create or revoke keys.
Domains
- Exact (
example.com) and wildcard (*.example.com) patterns. - Domain count enforced against the plan limit on add (Dev: 3 max).
- Matching performed server-side on every init call by
DomainMatcher. - Only workspace owners/admins can add or remove domains.
Team management
- Workspace members with roles:
owner,admin,member. - Owners can assign/remove the owner role; only owners can demote the last owner (blocked).
- Invitations — email-based, 7-day expiry, token-based accept URL.
- Duplicate email check before creating invitation.
- Audit log written before mail send (so the log is always present even on SMTP failure).
- Accepting an invitation is atomic:
WorkspaceMemberinsert + invitation update + audit log in one DB transaction.
- Member count enforced against the plan limit on invitation send.
Subscriptions & billing
- Stripe integration via Laravel Cashier webhooks.
- Subscription statuses:
dev(free),trialing,active,past_due,canceled,unpaid,blocked. - Grace period for
past_due: configurable viaSTRIPE_GRACE_DAYS(default 7 days); editor init remains allowed until grace expires. blockedstatus set byBlockExpiredGraceSubscriptionsscheduled command.- Unknown Stripe statuses fail closed (treated as
unpaid) — no accidental free access from new Stripe status values. - Stripe customer creation is race-condition safe:
lockForUpdate()prevents duplicate customers under concurrent requests. - Billing page shows current plan, period, renewal date, cancel-at-period-end status, and plan comparison cards.
- Invoice paid email — workspace owner receives a notification after
invoice.paidStripe webhook. ProcessStripeEventqueued job: 8 retries with exponential backoff, 90-second timeout. Alert email on permanent failure (STRIPE_EVENT_PROCESSING_FAILED_ENABLED).- Plan upgrade restricted to
dev→ paid only (no free self-upgrade exploit between paid tiers). - Defensive pull resync — optional background job (
STRIPE_PULL_RESYNC_ENABLED) re-fetches subscription state from Stripe as a backstop for missed webhooks.
Usage analytics
Available to Pro and Agency plans only.
Workspace-level (/app/usage)
- Init events in the last 24h.
- 30-day daily chart (OK vs error).
- Top projects by init volume (30d).
- Top domains (30d).
- Top error codes (30d).
- Paginated recent failures table, filterable by error code, domain, or project.
Project-level (project detail page, all plans)
- 3 stat cards: init total, init last 24h, success rate (30d).
- Last 5 init events (timestamp, status, domain).
- "Show details" toggle reveals: 14-day daily rollup table + top domains (7d) + top errors (7d).
Data pipeline
- Raw events stored in
usage_events. RollupUsageDailycommand aggregates intousage_rollups_daily,usage_rollups_daily_domains,usage_rollups_daily_errors(runs nightly, re-processable with--since).PruneUsageEventscommand prunes old raw events.- All timestamps UTC; timezone-safe aggregation via
convert_tz.
Auth & accounts
- Registration with email verification (
MustVerifyEmail). - Login with rate limiting (10 req/min).
- Forgot password with rate limiting (5 req/min).
- Verified middleware on all app routes that mutate data (invitations, profile).
- Welcome email on registration.
- Session security —
SESSION_SECURE_COOKIE=truerequired in production. is_adminflag onUsermodel: gates Horizon dashboard and admin routes; hidden from Inertia browser props.
Admin panel (/admin/*)
Restricted to is_admin users via EnsureIsAdmin middleware.
- Users — list, search, export CSV, toggle
is_admin. - Workspaces — list, inspect members and subscriptions.
- Projects — list across all workspaces.
- Usage — cross-workspace analytics, export.
- Ops — system health, DB backup status, scheduled command log.
- Doctor — runs
readyeditor:doctor --strict; hidden from regular users.
Editor release management
php artisan editor:publish-release {version}— publishes a release from the source tree (legacy/public/by default) topublic/releases/{version}/with a manifest and integrity hashes.ReleaseCatalog::listSelectable()— returns up to 3 recent stable releases for the workspace selector; filters out non-X.Y.Zversions (test, pre-release).- Global default release version set via
EDITOR_RELEASE_VERSIONenv var. - Per-workspace override stored in
tenant_overrides; previous override deleted before inserting new one (no unbounded growth).
Ops
- DB backups — scheduled via
OPS_DB_BACKUPS_*env vars; configurable disk, retention, prune schedule. - Restore drill — optional weekly restore test (
OPS_DB_RESTORE_DRILL_ENABLED). - Queue — Laravel Horizon (Redis); gate restricted to
is_admin. - Request IDs — every request gets a
X-Request-Idheader set byRequestIdmiddleware; propagated into audit logs and usage events. - CORS —
EditorInitCorsmiddleware handles preflight for the init endpoint;EditorUsageCorsfor usage reporting.
Emails
All emails use the branded Laravel mail theme (logo, brand blue #2563eb, Privacy/Terms links in footer).
| Trigger | |
|---|---|
| Registration | Welcome email |
| Email change / new account | Email verification |
| Forgot password | Password reset link |
| Team invitation | Workspace invitation with accept link |
Stripe invoice.paid |
Invoice paid notification to workspace owner |
Security
- Rate limiting — login (10/min), registration (10/min), forgot-password (5/min).
- RBAC — owner/admin/member roles enforced by Laravel Policies on all team and project actions.
- Audit log —
audit_logstable records all significant actions: invitation sent/accepted/deleted, member role changed, project domain add/remove, editor release selected, API key created/revoked. - SRI — integrity hashes on all editor assets, validated client-side by the loader.
- Branded error pages — custom 404, 403, 500 pages (no Laravel default stack traces in production).
- React error boundary —
ErrorBoundarywraps the Inertia app; catches JS crashes and shows a branded fallback instead of a blank page. - OG image — branded
public/og-image.png(1200×630) used across all marketing pages.