Nexora is a fictional B2B SaaS customer support platform, built as a portfolio project to demonstrate WordPress development without third-party plugins — a custom block theme, two custom post types, native auth, and a role-gated agent dashboard, all on stock WordPress APIs.
🔗 View the static homepage snapshot — a frozen visual copy of the real homepage (see note below on live vs. static).
This repo holds the custom WordPress theme (nexora) that powers the site — the actual code written for this project. It does not include WordPress core or the SQLite database integration, since those aren't original work; install them normally to run the theme (see Running it yourself below).
A frozen, static HTML copy of the real homepage lives in /docs and is published via GitHub Pages — that's the link above. It's a genuine mirror of the actual rendered markup/CSS (not a mockup), so the layout is exactly what the real site looks like. It's static, though: sign-in, ticket submission, the customer account, and the agent dashboard need a real WordPress backend to function, which GitHub Pages can't run.
| Area | Pages | Notes |
|---|---|---|
| Marketing site | /, /pricing/, /about/ |
Custom block theme, theme.json-based |
| Help Center | /help-center/ |
kb_article CPT, 26 seeded articles |
| Support | /submit-a-ticket/ |
support_ticket CPT, native form handling |
| Customer auth | /sign-in/, /get-started/, /account/ |
WordPress core auth, no custom hashing |
| Agent dashboard | /support-dashboard/ |
Role-gated ticket queue, triage, replies |
- WordPress block theme —
theme.json-driven design tokens, block templates (templates/*.html), template parts (parts/header.html,parts/footer.html), PHP-driven patterns (patterns/*.php) - Custom post types & statuses —
support_ticket,kb_article, with custom post statuses (ticket_open,ticket_in_progress,ticket_resolved,ticket_closed) instead of a bespoke status table - Custom PHP (
inc/) — ticket submission, agent dashboard (triage, assignment, internal notes, customer replies), customer auth and account pages - Native WordPress APIs only — no third-party plugins; auth via
wp_signon()/wp_insert_user(), forms viaadmin-post.php, messaging via the comment system
No third-party plugins. Everything runs on core WordPress APIs — custom post types, post statuses, post meta, comments, and the native user/auth system. This was a constraint, not a limitation: the goal was to show what's possible with just WordPress's own primitives.
Tickets are a CPT, not a plugin's custom table. support_ticket is a public => false CPT (see inc/support-tickets.php). Custom post statuses (ticket_open, ticket_in_progress, ticket_resolved, ticket_closed) replace a bespoke status column, so all of WP's post machinery (queries, meta, capabilities, revisions) works for free.
post_author is the customer relationship. Rather than inventing a "customer ID" meta field, a ticket's post_author is literally the WordPress user who filed it. Ownership checks are just post_author === get_current_user_id() — no extra table, no extra join, and it's a pattern any WordPress developer already knows.
Comments, repurposed, carry the conversation. Internal agent notes and customer-visible replies are both stored as wp_comments rows on the ticket, distinguished by comment_type (ticket_note vs ticket_reply) and hidden from normal comment queries via a pre_get_comments filter — the same trick WooCommerce uses for order notes. This avoided a second custom table for "messages" when the comment system already models "a list of dated, attributed text entries on a post" perfectly.
Capabilities, not custom roles. current_user_can('edit_others_posts') is the one boundary used everywhere to mean "this person is a support agent" — for dashboard access, for the reply/note endpoints, and for the assignee picker. It's a real WordPress capability already granted to Editors and Admins, so there's no custom role to maintain and no privilege-escalation surface from a misconfigured role.
admin-post.php, not REST/AJAX. Every mutating action (submit ticket, update status, add note, send reply, sign in, register) posts to admin-post.php with its own add_action('admin_post_{action}', ...) handler, nonce, and capability check. No JavaScript build step, no REST controller boilerplate — just forms and redirects, which is both simpler to demo and easier to explain than a client-side SPA layer would be.
Every handler checks its own authorization. A guard on the page (template_redirect) does not protect the endpoint — admin-post.php requests never run the page template. So every handler re-checks capability and nonce independently, rather than trusting that "you got this far so you must be allowed." This was verified with real attack requests, not just code review — see Security below.
Native auth only. Sign in / sign up / sign out go through wp_signon(), wp_insert_user(), wp_set_auth_cookie(), and wp_logout_url(). No custom password hashing, no custom session handling — WordPress already solved this correctly, so it isn't reinvented.
- Nonces on every mutating form; capability checks independent of page guards.
- Attack-tested: a logged-in customer hitting an agent-only endpoint gets
403; an anonymous request gets400(nonoprivhook registered); a forged nonce gets403. - A customer cannot view another customer's ticket by guessing/editing the
?ticket=URL parameter — ownership is re-checked server-side and a failed check silently falls back to the customer's own ticket list rather than showing an error (which would otherwise confirm the ticket number exists). - Agent replies always attribute the real agent internally (
comment_author) but always render as "Nexora Support Team" to the customer — deliberate, not an oversight, matching how real SaaS support tools anonymize the individual agent from the customer's point of view. - All output escaped (
esc_html,esc_attr,esc_url), all input sanitized (sanitize_text_field,sanitize_textarea_field,absint) at the point of use.
support_ticket (CPT, private)
├─ post_author → the customer (a WP user)
├─ post_status → ticket_open | ticket_in_progress | ticket_resolved | ticket_closed
├─ post_content → the customer's original message
├─ meta: _nexora_ticket_number → e.g. NX-1008 (customer-facing ID)
├─ meta: _nexora_ticket_category → account-login | billing-payments | technical-issue | integration | api | other
├─ meta: _nexora_ticket_priority → low | medium | high | urgent
├─ meta: _nexora_ticket_assigned_to → agent user ID
└─ comments (repurposed)
├─ comment_type = ticket_note → internal only, hidden from customer + default queries
└─ comment_type = ticket_reply → customer-visible, shown as "Nexora Support Team"
Email notifications, bulk ticket actions, SLA timers, custom agent roles, reporting/analytics. These would be natural next steps for a production product but weren't needed to demonstrate the architecture.
This theme was built and tested with WordPress Studio (local WordPress + SQLite). To run it:
- Create a WordPress site (Studio, Local, or any standard WP install)
- Copy this repo's contents into
wp-content/themes/nexora/ - Activate the theme
- Create the required pages (
/pricing/,/help-center/,/submit-a-ticket/,/sign-in/,/get-started/,/account/,/support-dashboard/,/about/,/contact-support/) — block-theme templates auto-map to a page by its slug (page-{slug}.html)
Nexora is a fictional B2B SaaS product created as a portfolio project — not a real business. Product copy, sample tickets, and knowledge-base articles are original but invented for demonstration purposes.