Voxel Guide — Product & Technical Specification

Target stack: Laravel 13 · PHP 8.3+ · Livewire 3 · Alpine · Tailwind 4 · PostgreSQL 16 · Redis · Meilisearch · Stripe Connect Replaces: WordPress + Voxel theme + Elementor (voxel.guide) and its successor build (stagingvoxel.wp1.site) Document status: v1.0 — derived from live audit of both sites, July 2026

1. What Voxel Guide is

Voxel Guide is an independent community hub and multi-vendor marketplace for the Voxel WordPress theme ecosystem. It does three jobs at once, and the rebuild must keep all three first-class:

  1. A discovery library. Curated showcases of sites built with Voxel, plus code snippets, automations, and how-to content. This is the SEO engine and the top of the funnel.
  2. A marketplace. Independent developers sell addons and assets; the platform takes a fee. Today this is Voxel's built-in product fields; the rebuild makes it a real Stripe Connect marketplace with licensing.
  3. A talent network. Developer profiles, ratings, a job board, and a quote/lead flow.

The site carries an explicit disclaimer — "We have no affiliation with the Voxel team but this site was made with their permission." — which must be preserved verbatim in the footer.

1.1 Audit summary of the current system

Both sites run WordPress with the Voxel theme, Elementor, and ~30–40 plugins. Live content counts pulled from the production sitemap:

Content type Prod URL prefix Prod count On staging
Website showcase /website/{slug} 120 yes
Snippet /snippet/{slug} 114 yes
Asset /asset/{slug} 102 yes
Article (Tips/How-to/FAQ) /article/{slug} 19 yes
Job /job/{slug} 1 yes
Page /{slug} 47 yes
Post /{slug} 2 yes
Addon /addon/{slug} staging only
Deal /deal/{slug} staging only
Workflow /workflow/{slug} staging only
Meetup /meetup/{slug} staging only
Profile /profile/{username} staging only

Read this carefully: staging is not a superset of production by content, only by schema. Staging introduced five content types that production never had, while production kept accumulating real submissions (its website sitemap has 120 entries vs. staging's ~103). The migration therefore has to merge two diverging datasets, not simply promote staging. See MIGRATION.md §3.

Notable plugin dependencies that encode business logic (each needs an explicit disposition):


2. Roles and permissions

Role Can
Guest Browse everything except gated outbound links; see prices; cannot see live site URLs on showcases (current behaviour: "Login to View Website Link")
Member Submit showcases/snippets/workflows, save & collect, vote, comment, review purchases, message, build quotes, check in to meetups
Vendor Everything a Member can, plus: sell assets/addons, manage releases & license keys, view sales/payout dashboards, respond to reviews. Requires completed Stripe Connect onboarding
Employer Post and manage jobs, review applicants. Any Member can become one by posting
Moderator Approve/reject submissions, verify showcases, handle reports, edit any listing, merge duplicates
Admin All of the above, plus fee configuration, payouts, refunds, feature flags, user administration

Implemented as a roles/permissions pair (spatie/laravel-permission) with policy classes per model. Vendor is a capability flag earned via Stripe onboarding, not merely a role assignment — gate on $user->vendor?->payouts_enabled.


3. Domain model

3.1 Shared concerns

Nearly every content type shares the same behaviours. Model these once as traits + polymorphic tables rather than duplicating columns eleven times:

Status lifecycle (every submittable type): draft → pending → published → archived, plus rejected and spam. Transitions emit events; pending → published is the moderation gate.

3.2 Content types

websites — showcase

The flagship SEO asset. Fields observed live: gallery images, description, live URL, category, plugins used, page builder, view count, verified badge with timestamp, submitter, save, vote, add-to-quote.

websites
  id, slug, title, description, live_url, screenshot_id
  user_id            (submitter)
  claimed_by_id      (nullable — the "Claim Posts" behaviour)
  verified_at, verified_by_id
  views, votes_count, status, published_at

Taxonomies: website_type (Business/Corporate, E-Commerce, Directory/Contact, Community Forum, Booking, Non-Profit, Blog, Event, Marketplace), plugins_used, page_builder.

live_url is gated: guests see a "Sign in to view" affordance. Preserve this — it drives signups. Store outbound clicks in link_clicks for vendor analytics.

snippets

snippets
  id, slug, title, instructions (markdown), screenshot_id
  user_id, views, votes_count, status, published_at
snippet_blocks              -- a snippet has MANY code blocks; drives the "+1 / +2" card badges
  id, snippet_id, language (php|js|css|html|sql|json), label, code, position

Server-side syntax highlighting at save time (Shiki via a queued job, cached as HTML) so pages ship zero highlighting JS. Copy-to-clipboard is Alpine, per block and for all blocks.

assets and addons — the sellable types

These share a product concern. An asset is a downloadable file (template, JSON export, config); an addon is a distributable plugin with versions and licensing. Both may be free or paid.

products                        -- polymorphic parent for asset/addon
  id, productable_type, productable_id
  vendor_id, price_cents, currency, is_free
  license_type (single|extended|unlimited), activation_limit
  external_url                  -- some addons sell off-site; keep as a link-out product
  sales_count, rating_avg, rating_count
product_versions
  id, product_id, version, changelog, file_path, file_size, min_voxel_version, released_at
product_files                   -- for multi-file assets
license_keys
  id, product_id, order_item_id, user_id, key, activation_limit, status, expires_at
license_activations
  id, license_key_id, site_url, activated_at, deactivated_at

Taxonomies: template_type (assets), addon_category, compatibility (Voxel version ranges).

Design note: production sells addons that live on the vendor's own site (external_url). Don't force those into checkout — model them as listing-only products with an outbound CTA, and let vendors upgrade to on-platform selling later. This is the difference between migrating 100% of the catalogue and migrating 40% of it.

jobs

jobs
  id, slug, title, description, employer_id
  project_type (one_time|recurring)
  experience_level (beginner|intermediate|advanced)
  payment_type (fixed|hourly)
  budget_min_cents, budget_max_cents, currency
  location, is_remote, applications_count
  status, published_at, expires_at
job_applications
  id, job_id, user_id, cover_letter, proposed_rate_cents, status

profiles — vendor / developer

Observed fields: avatar, local time, overall rating plus three sub-scores, about, skills, hourly rate, location, member-since, for-hire flag, per-type portfolio counts, social links, message, check-in.

profiles
  id, user_id, display_name, headline, bio, avatar_id, cover_id
  location, timezone, hourly_rate_cents, is_for_hire
  website, social (jsonb: facebook, youtube, twitter, linkedin, instagram)
  rating_avg, rating_communication, rating_accuracy, rating_value, rating_count
profile_skills   -- pivot to a curated `skills` table (CSS, HTML, PHP, JS, AI, APIs,
                 -- Payment Gateways, Plugin Creation, Dynamic Conditions, Site Speed, Image Optimization…)

Portfolio counts are derived, cached counters, not stored truth — recompute on listing publish.

deals, workflows, meetups, articles

Lighter types, all built on the Listing base.

3.3 Commerce

orders           id, user_id, number, subtotal_cents, fee_cents, tax_cents, total_cents,
                 currency, status, stripe_payment_intent_id, placed_at
order_items      id, order_id, product_id, vendor_id, unit_price_cents, platform_fee_cents,
                 vendor_amount_cents, stripe_transfer_id
vendors          id, user_id, stripe_account_id, payouts_enabled, charges_enabled,
                 fee_percent (nullable override), payout_schedule
payouts          id, vendor_id, amount_cents, period_start, period_end, stripe_payout_id, status
refunds          id, order_item_id, amount_cents, reason, stripe_refund_id, status
reviews          id, product_id, user_id, order_item_id, rating, body, vendor_reply, published_at
quotes           id, user_id, session_id, status          -- the "Add to Quote" basket
quote_items      id, quote_id, quotable_type, quotable_id, note

Reviews require a matching order_item_id (or a free download record) — verified purchases only.

3.4 Cross-cutting

users, sessions, notifications, saves, collections, votes, comments, reports,
media, taxonomy_terms, taxonomables, messages, conversations, link_clicks,
redirects (legacy URL → new URL), activity_log

4. Information architecture & routes

/                                   home — discovery-led
/websites                           showcase archive (filters: type, plugins, page builder)
/websites/{slug}
/snippets            /snippets/{slug}
/assets              /assets/{slug}
/addons              /addons/{slug}
/jobs                /jobs/{slug}          /jobs/post
/workflows           /workflows/{slug}
/deals               /deals/{slug}
/meetups             /meetups/{slug}
/hire                                developer directory (filters: skills, rate, availability, rating)
/@{username}                         public profile — short, memorable, replaces /profile/{u}
/learn/tips  /learn/videos  /learn/faqs  /learn/walkthrough
/search                              unified search results
/submit/{type}                       submission wizard
/cart  /checkout  /orders/{number}
/dashboard                          member: saves, collections, submissions, orders, licenses, messages
/dashboard/vendor                   vendor: products, releases, sales, payouts, reviews
/admin                              moderation queue, users, fees, reports
/fees                               fee calculator (marketing tool — keep)

Slug change: singular /website/x → plural /websites/x aligns every type and reads better, but it is a 500+ URL redirect map. It is worth doing once, at migration, never later. All legacy paths are handled by the redirects table (§7.3).

/profile/{u}/@{u} likewise, with permanent redirects.


5. Key user flows

5.1 Submit a showcase

Multi-step Livewire wizard, autosaving to draft on every step:

  1. URL → server fetches title, favicon, OG image, and captures a screenshot (queued, Browsershot/ Playwright) so the user isn't asked to upload one
  2. Category + plugins + page builder (typeahead against existing terms; new terms need moderation)
  3. Description + extra gallery images
  4. Review → submit → pending

Moderator sees it in /admin/queue. Approve → published + notification + optional social post.

5.2 Buy an asset

  1. Add to cart (guest cart in session, merged on login)
  2. Checkout — Stripe Payment Element; tax via Stripe Tax
  3. PaymentIntent succeeds → OrderPlaced job:
    • split each item: platform_fee_cents = round(price * fee_percent) via Stripe transfer_data
    • issue license_key per licensed item
    • grant download entitlement
    • notify buyer + each vendor
  4. Buyer downloads from /dashboard/licenses — signed, expiring URLs, activation-limited

5.3 Become a vendor

/dashboard/vendor/onboard → create Stripe Connect Express account → hosted onboarding → webhook account.updated flips charges_enabled/payouts_enabled → product creation unlocks. Until then, vendors may draft products but not publish paid ones.

5.4 Hire a developer

Browse /hire → filter → profile → Message (in-app conversation) or Request a quote (attaches current quote basket). Quote requests notify by email + in-app, and are tracked to completion so ratings can be requested afterwards.


Global ⌘K palette is a signature of the current site and must survive. Back it with Meilisearch via Laravel Scout:

Archive filtering uses the same index so filter + search never disagree. Postgres FTS is the documented fallback if a managed Meilisearch is not desired — same Scout interface, one env var.


7. Non-functional requirements

7.1 Performance

7.2 Accessibility

The current site pays for accessiBe, an overlay. Do not carry that forward — overlays are widely regarded as ineffective and have drawn litigation. Build to WCAG 2.2 AA natively: semantic landmarks, visible focus rings, keyboard-operable filter and palette, prefers-reduced-motion honoured, 4.5:1 contrast minimum. This is a cost saving and a quality improvement simultaneously.

7.3 SEO — the highest-risk area of this project

The site's value is 500+ indexed pages. Losing them loses the business.

7.4 Security


8. Design system

Driven by the existing brand mark: teal #2D7492 and ink #282530, both lifted from the staging logo.

Every colour, radius, and font in the build is a CSS custom property under a single :root block, so a rebrand is one file. The token contract is identical across all three design directions — that is what makes them swappable:

--brand  --brand-ink  --brand-soft  --brand-strong
--ink  --ink-2  --ink-3
--surface  --surface-2  --surface-3
--line  --line-strong
--accent  --ok  --warn  --danger
--radius  --radius-lg  --shadow-1  --shadow-2
--font-display  --font-body  --font-mono

Tailwind 4 consumes these via @theme, so utilities and custom CSS stay in sync. Dark mode is a second :root[data-theme="dark"] block — no utility duplication.

The logo ships as inline SVG with fill="var(--brand)" and fill="var(--ink)", so it recolours with the theme instead of needing per-theme PNG exports.


9. Scope boundary — what stays on WordPress

Per decision, this rebuild covers content + marketplace only. These stay where they are and are integrated, not replaced:

System Disposition
FluentCommunity Stays. Integrate via SSO so voxel.guide and the community share one identity
Fluent Support Stays. Deep-link from /dashboard; pass user context on handoff
FluentCRM Stays as the ESP. Laravel emits events (signup, purchase, submission) to FluentCRM over its REST API

Required to make this work: a single sign-on bridge. Laravel becomes the identity provider; the WordPress install authenticates against it via a signed-JWT plugin. Without this the user experience splits in two and the whole thing feels broken. Budget it as real work — it is roughly a two-week task, not an afternoon.


10. Build phases

Phase Contents Est.
0 Repo, CI, staging infra, design system, auth, roles, media pipeline 2 wk
1 Listing base, websites + snippets + articles, archives, search, submissions, moderation 4 wk
2 Profiles, /hire, messaging, saves/collections/votes 3 wk
3 Marketplace: products, Connect onboarding, cart, checkout, licensing, vendor dashboard 5 wk
4 Jobs, deals, workflows, meetups, quote basket 3 wk
5 SSO bridge to WordPress, CRM eventing, analytics 2 wk
6 Migration dry runs, redirect map, SEO parity checks, launch 3 wk

~22 weeks of focused work. Phases 1–2 are independently shippable behind a beta flag.


11. Open questions

  1. Addon catalogue economics. Most addons currently link out to vendor sites. What share do you expect to convert to on-platform selling? If it is low, the licensing system is over-built for v1 and should move to Phase 5.
  2. Existing passwords. WordPress uses phpass. Laravel can verify these on first login and rehash transparently — but only if the wp_users hashes come across. Confirm DB access. Otherwise every user needs a reset email, which will cost some percentage of the userbase.
  3. Which dataset wins. Production has more real content; staging has more schema. §3 of MIGRATION.md proposes prod-as-truth. Confirm.
  4. Merch store. Currently empty. Drop it, or keep a print-on-demand link-out?
  5. Fee percentage. 15% is assumed throughout. Confirm before the payout maths is written.