Skip to content

Latest commit

 

History

429 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Innovayse

A modern, self-hosted web hosting management platform — an open-source alternative to WHMCS. Built with ASP.NET Core 9, Nuxt 4, and Vue 3.

License: Business Source License 1.1 — free to self-host, source-available, converts to GPL v2 on 2028-05-15.


🌐 Live Demo

https://hostpanel.innovayse.com

Role Email Password
Admin superadmin@hostpanel.com Admin123!
Reseller reseller@hostpanel.com Admin123!
Client customer@hostpanel.com Admin123!

Features

  • Client Portal — order services, manage domains, view invoices, open support tickets
  • Admin Panel — full control over clients, products, billing, provisioning, and integrations
  • Billing — invoices, payment gateways (Stripe, PayPal, bank transfer)
  • Domain Management — registrar integrations (Namecheap, NameAm)
  • Hosting Provisioning — cPanel/WHM and CWP support
  • Multi-language — English, Russian, Armenian (hy)
  • Plugin SDK — extend functionality via the Innovayse.SDK
  • WHMCS Migration — import clients and data from existing WHMCS installations

Tech Stack

Layer Technology
Backend API ASP.NET Core 9, Wolverine, EF Core 8, PostgreSQL
Client Portal Nuxt 4, Vue 3, TypeScript, Tailwind CSS, Pinia
Admin Panel Vite, Vue 3, TypeScript, Tailwind CSS, shadcn-vue
Messaging RabbitMQ + Wolverine outbox
Auth ASP.NET Core Identity, JWT + Refresh Tokens
Email MailKit (dev: MailHog)
Docs Scalar

Requirements

  • Docker and Docker Compose v2
  • (For local development without Docker) .NET 9 SDK, Node.js 22+, yarn, PostgreSQL 17

The client portal needs Node 22, not 20: Nuxt's nitropack pulls rollup-plugin-visualizer@7, which declares engines.node >= 22, and yarn install refuses on anything older. docker/client.Dockerfile already uses node:22-alpine.


Quick Start

# 1. Clone the repository
git clone https://github.com/innovayse/hostpanel.git
cd innovayse

# 2. Copy and configure environment variables
cp .env.example .env
# Edit .env — set JWT_SECRET, and NUGET_INNOVAYSE_CREDENTIALS if you are building
# the API image yourself (see below)

# 3. Start all services
docker compose up -d
# The API applies pending migrations on startup, so there is no separate step.
# Follow it with: docker compose logs -f hostpanel-api

# 4. Open in browser
#   Client portal:  http://localhost:3001
#   Admin panel:    http://localhost:5174
#   API docs:       http://localhost:5149/scalar
#   MailHog (dev):  http://localhost:8027

The API depends on Innovayse.Auth, published to a private package registry, and the image restores it while it builds. Set NUGET_INNOVAYSE_CREDENTIALS in .env to a deploy token scoped to read_package_registry — the format is in .env.example. Without it the build stops at NU1301 … 401 (Unauthorized).


Environment Variables

Copy .env.example to .env and fill in the values:

Variable Description Default
POSTGRES_HOST PostgreSQL host the API connects to hostpanel-db
POSTGRES_PORT PostgreSQL port 5432
POSTGRES_USER PostgreSQL username postgres
POSTGRES_PASSWORD PostgreSQL password postgres
POSTGRES_DB Database name innovayse_dev
JWT_SECRET JWT signing key (min 32 chars) change this
RABBITMQ_HOST Broker host the API connects to rabbitmq
RABBITMQ_PORT Broker AMQP port 5672
RABBITMQ_USER RabbitMQ username guest
RABBITMQ_PASSWORD RabbitMQ password guest
SMTP_HOST SMTP server host mailhog
SMTP_PORT SMTP server port 1025
SMTP_USERNAME SMTP username; empty for a local catcher, which refuses AUTH (empty)
SMTP_PASSWORD SMTP password (empty)
SMTP_ENCRYPTION tls (STARTTLS, 587), ssl (implicit TLS, 465) or none. Required — the API refuses to start without it rather than guess from the port none
SMTP_FROM Sender address on outgoing mail noreply@innovayse.com
SMTP_FROM_NAME Sender display name Innovayse

For the client portal, copy client/.env.example to client/.env.


Authentication modes

AUTH_MODE decides how people sign in. .env.example ships sso; a standalone box sets local.

sso local
Client portal Innovayse SSO email and password, against this database
API SSO tokens JWT from POST /api/auth/login
Admin panel works cannot be signed into — see below

The admin panel needs SSO today

Its sign-in page offers one button, "Sign in with Innovayse SSO", and the SPA holds no token of its own: it asks GET /api/auth/me on every load and reads a session cookie it cannot see. That cookie is issued by the API's OIDC exchange, which only runs under AUTH_MODE=sso.

Under local there is no such cookie. POST /api/auth/login succeeds and returns a JWT — the client portal uses it — but nothing in the admin SPA sends a bearer token, so /api/auth/me answers 401 and the panel stays on its login screen. sso-api, the service that would issue the cookie, is addressed by docker-compose.yml but defined in the platform overlay rather than here, so docker compose up on its own never starts one.

Nothing in the panel is broken; there is simply no way in. Until that changes, a standalone deployment administers itself through the API, with a token from POST /api/auth/login:

TOKEN=$(curl -s -X POST http://localhost:5149/api/auth/login \
  -H 'Content-Type: application/json' -H 'X-Requested-With: XMLHttpRequest' \
  -d '{"email":"you@example.com","password":"…"}' | jq -r .accessToken)

curl -s http://localhost:5149/api/products -H "Authorization: Bearer $TOKEN"

Closing this properly means choosing between issuing a session cookie in local mode and letting the SPA carry a bearer token again — the second being what the SPA was deliberately moved away from. That is a design decision, not an oversight, and it is tracked as an issue rather than guessed at here.


Project Structure

innovayse/
├── backend/                  # ASP.NET Core solution
│   └── src/
│       ├── Innovayse.API/        # Controllers (thin, Wolverine dispatch)
│       ├── Innovayse.Application/ # CQRS handlers, validators, DTOs
│       ├── Innovayse.Domain/      # Entities, value objects, interfaces
│       ├── Innovayse.Infrastructure/ # EF Core, repositories, integrations
│       ├── Innovayse.Providers.CWP/  # CWP provisioning provider
│       └── Innovayse.SDK/         # Plugin SDK
├── client/                   # Nuxt 4 client portal
│   ├── pages/                    # Routes: data, SEO, business logic
│   ├── templates/                # Storefront designs — presentation only
│   │   ├── registry.ts               # name + slot → component
│   │   ├── aurora/                   # Default design
│   │   └── classic/                  # Original design
│   └── components/               # Shared component library
├── admin/                    # Vue 3 admin panel
├── docker/                   # Dockerfiles and nginx config
├── docker-compose.yml
└── .env.example

Development

Backend

cd backend
dotnet restore
dotnet run --project src/Innovayse.API

appsettings.Development.json is committed and holds only local values, so this runs without copying a template first. It used to be an appsettings.Development.example.json you had to copy — which was a quiet hazard, because the copy is gitignored nowhere and went into the next commit along with whatever real credential had been filled into it.

Anything genuinely secret stays out of the repository. Use user-secrets rather than editing a settings file:

dotnet user-secrets init
dotnet user-secrets set "Sso:Authority" "https://sso.example.com"

Client Portal

cd client
yarn install
yarn dev        # http://localhost:3000

Admin Panel

cd admin
npm install
npm run dev     # http://localhost:5173

Storefront Templates

The public portal ships two designs and renders whichever one is selected. Both produce identical SEO output — canonical, hreflang and schema.org live in client/pages/, never in a template.

Set the site's own origin, or the sitemap will advertise whatever host each request happened to arrive on — behind a proxy that can be the container's internal address. NUXT_PUBLIC_BASE_URL covers it at build time; a prebuilt image reads NUXT_SITE_URL at startup, which overrides the built-in value.

Template Description
aurora Default. Dark and light modes, Armenian typography, live domain search
nova Conversion-focused hosting landing page: pricing, plan comparison, security, migration, FAQ
classic The original storefront design

nova supplies its own header, footer, homepage and hosting page, and reuses aurora's domain search and checkout — the checkout is the ordering path, and a second copy of it would be a second place for a payment bug to live. Its design tokens are prefixed --n-* and declared only under .tpl-nova and html[data-template='nova'], so activating it cannot disturb the other two.

Several of its cards state things this codebase cannot verify — NVMe storage, a CDN, server-side caching, a firewall, a malware scanner, a backup schedule, an uptime figure, free migration. Each waits on a setting and is hidden until an operator turns it on:

Setting Shows
portal.features.storage the SSD/NVMe storage card
portal.features.caching the server-side caching card
portal.features.cdn the CDN card
portal.features.firewall the firewall card
portal.features.malware the malware-protection card
portal.features.backups the backup cards, in the hero, trust bar and security grid
portal.trust.uptime the uptime figure in the trust bar, printed verbatim
portal.migration.free the "free migration" badge, when set to true

Any non-empty value turns the first six on; the uptime setting holds the figure itself. The comparison table and the testimonials section are data-driven the same way: the table renders only the specification lines entered under Admin → Products → Specification, and the testimonials section does not render at all, because nothing serves customer quotes yet.

Choosing one. In order of precedence:

  1. The portal.template setting, editable in Admin → Settings → Portal appearance
  2. The NUXT_PUBLIC_PORTAL_TEMPLATE environment variable
  3. aurora

An unrecognised value falls back to aurora rather than leaving the site blank, so a typo in the admin field cannot take the storefront down.

Adding a template. Create client/templates/<name>/ with layout/Header.vue, layout/Footer.vue and a pages/ component for each route, then register the loaders in client/templates/registry.ts and add the name to TEMPLATE_NAMES in client/templates/types.ts. A unit test asserts every template implements every slot, so a missing page fails the build rather than rendering nothing.

Two rules make templates safe to swap:

  • Templates render, pages decide. A template component takes typed props and returns markup; the page above it owns fetching, SEO and business logic. That keeps both designs on one code path and one set of head tags.
  • Copy lives in i18n under a top-level key named for the template (aurora.hero.title), registered in the modules array of client/plugins/i18n.ts. Locale files are not auto-discovered.

aurora follows the first rule throughout, with one exception: its pages/Checkout.vue still carries the ordering flow, because splitting a working payment path is only worth doing with a backend on hand to place a test order against. It is classic's script byte-for-byte with restyled markup, so the two cannot drift apart.

classic's pages are lift-and-shifts of the original route components and keep their own data loading. That was the point: moving a 500-line page unchanged is far safer than rewriting it, and the design it renders is the one being replaced. New templates should follow aurora, not classic.

Branding and colour mode, all from Admin → Settings with a NUXT_PUBLIC_PORTAL_* environment fallback for a first boot (the admin value wins once set):

Setting Env fallback What it does
portal.site.name NUXT_PUBLIC_PORTAL_SITE_NAME Site name for og:site_name, logo alt text and structured data. Empty shows the built-in name.
portal.site.tagline NUXT_PUBLIC_PORTAL_SITE_TAGLINE Default meta description.
portal.theme.default NUXT_PUBLIC_PORTAL_THEME_DEFAULT light, dark or system for a visitor with no saved choice. A visitor's own choice is kept in a color-mode cookie and rendered on the server, so there is no flash of the wrong theme.
portal.theme.user_toggle NUXT_PUBLIC_PORTAL_THEME_USER_TOGGLE false hides the light/dark switch.
portal.logo NUXT_PUBLIC_PORTAL_LOGO Logo for light backgrounds.
portal.logo.dark NUXT_PUBLIC_PORTAL_LOGO_DARK Logo for dark backgrounds; falls back to the light one.
portal.logo.mark NUXT_PUBLIC_PORTAL_LOGO_MARK Square icon-only logo for compact places.
portal.favicon NUXT_PUBLIC_PORTAL_FAVICON Browser tab icon; an upload generates the whole icon set.

Logos and the favicon can be uploaded in Admin → Settings → Branding or pasted as URLs. portal.template and portal.theme.default accept only their listed values; anything else is refused with INVALID_SETTING_VALUE.

Related operator settings, all optional and hidden when empty: portal.contact.whatsapp, portal.contact.telegram, portal.contact.email, portal.contact.phone, portal.chat.provider, portal.newsletter.action_url, portal.legal.tax_id, and portal.social.{facebook,instagram,linkedin,youtube}.

The header app launcher is off unless portal.apps.enabled is true, because the apps it links to only exist in a deployment that runs them. Each entry then needs a URL of its own — portal.apps.{account,tasks,erp,hostpanel,sheets,mail,docs,calendar}, or the matching NUXT_PUBLIC_* variable — and entries without one are left out.

Every seeded setting is editable in Admin → Settings; the table takes one row at a time.

The plans page's comparison table is built from each product's specification lines, edited in Admin → Products → Specification. Plans line up by feature name, so the same line has to be spelled the same way across products; a plan that omits a line shows a dash rather than shifting its column. With no product specified, the table is not rendered at all.


Integrations

Innovayse uses a pluggable provider model. Implement the relevant interface in Innovayse.Domain and register in Infrastructure:

Interface Purpose Built-in Providers
IPaymentGateway Payment processing Stripe
IRegistrarProvider Domain registration Namecheap, NameAm
IProvisioningProvider Hosting provisioning cPanel/WHM, CWP

Contributing

See CONTRIBUTING.md.


License

Copyright (c) 2024 Innovayse. Licensed under the Business Source License 1.1. On 2028-05-15 this software will become available under the GNU GPL v2.0 or later.

About

Open-source web hosting management platform — self-hosted alternative to WHMCS

Topics

Resources

Contributing

Stars

11 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages