- Clone and enter the repo.
- Run bootstrap once.
- Set your local DB connection string.
- Run the app in Development mode.
Clone:
git clone <your-repo-url> vortex-cloud
cd vortex-cloudBootstrap (PowerShell):
pwsh -File scripts/bootstrap.ps1Bootstrap (bash/zsh):
sh scripts/bootstrap.shSet Vortex:Database:ConnectionString in appsettings.Development.json, then run:
PowerShell:
$env:DOTNET_ENVIRONMENT="Development"; dotnet run --project Vortex.Main/Vortex.Main.csprojbash/zsh:
DOTNET_ENVIRONMENT=Development dotnet run --project Vortex.Main/Vortex.Main.csprojVortex.Cloud.sln is the main Vortex emulator solution.
It includes the host executable (Vortex.Main), domain modules (Vortex.Rooms, Vortex.Players, Vortex.Database, and others), networking/message layers, and plugin infrastructure.
- .NET SDK 9.x (pinned via
global.json) - Git
- MySQL running locally (or reachable dev instance)
Check SDK:
dotnet --versionappsettings.jsoncontains shared defaults.appsettings.Development.jsonis local-only and gitignored.- The bootstrap script creates
appsettings.Development.jsonfromappsettings.jsonif missing.
Dockerfile, .dockerignore and docker-compose.yml at the repository root build and run
Vortex.Main plus a MySQL 8 instance. They are build/run artefacts only — no application code
depends on them, and running the emulator from the SDK as described above is unaffected.
docker compose build
docker compose upThe vortex service waits on depends_on: mysql: condition: service_healthy. This is required,
not cosmetic: Vortex.Database/Extensions/ServiceCollectionExtensions.cs resolves the server
version with ServerVersion.AutoDetect(connectionString) whenever
Vortex:Database:MySqlServerVersion is unset (the default). AutoDetect opens a real MySQL
connection during DI configuration, before the host is built and outside the reach of
EnableRetryOnFailure, so a database that is not yet accepting connections is a hard startup
failure rather than a retried transient.
The schema is not created for you. Migrations are never applied at host startup — see
Applying migrations below. On a fresh volume, start the stack, apply the
migrations once, then restart vortex.
| Host port | Container | Service |
|---|---|---|
| 30000 | vortex |
Game client, raw TCP socket (SuperSocket) |
| 30001 | vortex |
Game client, WebSocket socket (SuperSocket) |
| 8080 | vortex |
Vortex web API — Swagger UI at http://localhost:8080/swagger |
| 9000 | vortex |
Operator dashboard SPA + API — http://localhost:9000, Swagger at /swagger |
| 3307 | mysql |
MySQL 8 (3307 on the host to avoid a local MySQL already on 3306) |
The Orleans silo (11111) and gateway (3000) ports are deliberately not published: the stack runs a single in-process silo.
MySQL data lives in the named volume vortex-mysql-data, so docker compose down keeps the
schema and docker compose down -v destroys it.
Vortex.Primitives/Hosting/ListenerSecurity.cs refuses to start an HTTP listener bound to a
non-local address with HTTPS disabled, because both HTTP surfaces carry credentials and a session
cookie. Its IsLocalHost treats 0.0.0.0, ::, * and + as not local — they accept
traffic on every interface, which is precisely the remote-exposure case.
Inside a container, a listener bound to 127.0.0.1 is unreachable: published ports are forwarded
to the container's external interface. The listeners must bind 0.0.0.0, so the guard fires.
The compose file does not bypass or modify the guard. It sets the two opt-in keys the code already reads:
| Configuration key | Declared in | Environment variable in docker-compose.yml |
|---|---|---|
Vortex:WebApi:AllowInsecureRemoteHttp |
Vortex.WebApi/Configuration/WebApiConfig.cs |
VORTEX__Vortex__WebApi__AllowInsecureRemoteHttp |
Vortex:Observability:DashboardAllowInsecureRemoteHttp |
Vortex.Observability/Configuration/ObservabilityConfig.cs |
VORTEX__Vortex__Observability__DashboardAllowInsecureRemoteHttp |
With those set, ValidateListener returns AllowedWithWarning instead of Refused, and the
warning is still logged on every start.
This is a local development setting only. It is acceptable here because the published ports
are meant to be reached from localhost on the developer's machine. Anywhere the ports are
reachable from a network, remove those two variables and either enable HTTPS on the listeners or
bind them to a local address behind a TLS-terminating reverse proxy.
Note that the two game sockets are configured through unprefixed variables
(serverOptions__TcpServer__listeners__0__ip, serverOptions__WebSocketServer__listeners__0__ip).
Vortex.Networking/NetworkManager.cs builds them with SuperSocketHostBuilder.Create(), which
bottoms out in Host.CreateDefaultBuilder(args) — a separate generic host with its own
configuration root. The VORTEX__ prefix registered in Vortex.Main/Program.cs does not reach it;
CreateDefaultBuilder reads unprefixed environment variables. ListenerSecurity does not police
these two — it guards the HTTP surfaces only.
Migrations are not applied automatically when the host starts; when to migrate is an operations
decision. Apply them explicitly against the running mysql service.
The runtime image is intentionally SDK-free, so run the EF tooling from a throwaway SDK container joined to the compose network, with the repository mounted:
docker compose up -d mysql
docker run --rm \
--network vortex-cloud_default \
-v "$PWD:/src" -w /src/Vortex.Database \
-e Vortex__Database__ConnectionString="Server=mysql;Port=3306;Database=vortex;User Id=vortex;Password=vortex-dev;" \
-e Vortex__Database__ServerVersion="8.0-mysql" \
mcr.microsoft.com/dotnet/sdk:10.0 \
sh -c 'dotnet tool install --global dotnet-ef && export PATH="$PATH:/root/.dotnet/tools" && dotnet ef database update'PowerShell: replace $PWD with ${PWD} and the line continuations with backticks.
Details that matter:
dotnet efis not in.config/dotnet-tools.json, hence thedotnet tool installin the command.- The working directory must be a project directory:
VortexDbContextFactoryreadsappsettings.jsonfromDirectory.GetCurrentDirectory()/.., i.e. the repository root. - Both variables are unprefixed. That factory builds its own configuration with a plain
AddEnvironmentVariables(), soVORTEX__…would be ignored, and it readsVortex:Database:ServerVersion(notMySqlServerVersion, which is the runtime host's key). Pinning the version skipsAutoDetect.
From the host instead of a container, the same thing works against the published port:
cd Vortex.Database
Vortex__Database__ConnectionString="Server=127.0.0.1;Port=3307;Database=vortex;User Id=vortex;Password=vortex-dev;" \
Vortex__Database__ServerVersion="8.0-mysql" \
dotnet ef database updateEvery credential in docker-compose.yml is a development value committed to the repository, so it
is public: the MySQL passwords, the RSA handshake keypair and the IP-hash secret. The compose file
is a local dev stack, not a deployment.
To run this anywhere else:
- Move every
VORTEX__…value out of the compose file. It reaches the host throughAddEnvironmentVariables(prefix: "VORTEX__")inVortex.Main/Program.cs, so any mechanism that sets process environment variables works: an untracked.envfile referenced withenv_file:(.envis already in.dockerignore), Docker/Swarm secrets read into the variable at entrypoint, or the orchestrator's own secret store (KubernetesSecretviaenvFrom, ECS task-definition secrets, systemdEnvironmentFile=). - Generate a fresh RSA keypair —
openssl genrsa -3 1024— and configure the client with the matching modulus.CryptoConfigValidatorrejects theCHANGE_MEplaceholders, so a half-configured host fails at startup rather than at the first handshake. - Set a real
Vortex:Authentication:IpHashSecret. Outside Development the validator refuses the repository defaults, since a known HMAC key makes the hashed IPs in auth events reversible. - Drop
DOTNET_ENVIRONMENT: Development. Outside Development,Vortex.Main/Extensions/HostApplicationBuilderExtensions.csrefuses to start Orleans with localhost clustering plus in-memory grain storage, which is what this single-container stack uses. A real deployment needsVortex:Orleans:ClusteringProviderandVortex:Orleans:GrainStorageProviderset toadonet— or an explicitAllowUnclusteredOutsideDevelopmentif a single node with non-durable Orleans state is genuinely what you want. - Drop the two
AllowInsecureRemoteHttpopt-ins and terminate TLS in front of the listeners.
- Fast local commit check:
dotnet build Vortex.Main/Vortex.Main.csproj -t:VortexCloudFastCheck
- Full quality gate (pre-push + CI):
dotnet build Vortex.Main/Vortex.Main.csproj -t:VortexCloudQualityGate
- AI policy rollout phase:
- Default is
VortexAIPolicyPhase=1(warn-first). - Preview strict mode with
-p:VortexAIPolicyPhase=2.
- Default is
Hooks are repository-managed in .githooks:
pre-commitruns the fast check.pre-pushruns the full quality gate.
| Command | Scope | Default? | Use when |
|---|---|---|---|
dotnet build Vortex.Main/Vortex.Main.csproj |
Core emulator only | Yes | Normal core development and CI-compatible local checks |
dotnet build Vortex.Cloud.sln |
All projects currently in solution (including sample plugin) | No | One-window integrated core + plugin work |
dotnet build ../turbo-sample-plugin/TurboSamplePlugin/TurboSamplePlugin.csproj |
Sample plugin only | No | Plugin-only iteration |
TurboSamplePlugin intentionally stays in Vortex.Cloud.sln for IDE convenience, but the default repo build contract is project-scoped to Vortex.Main.
- Core build (default):
dotnet build Vortex.Main/Vortex.Main.csproj - Integrated solution build (optional):
dotnet build Vortex.Cloud.sln - Plugin build only (optional):
dotnet build ../turbo-sample-plugin/TurboSamplePlugin/TurboSamplePlugin.csproj - Fast checks:
dotnet build Vortex.Main/Vortex.Main.csproj -t:VortexCloudFastCheck - Full quality gate:
dotnet build Vortex.Main/Vortex.Main.csproj -t:VortexCloudQualityGate - Run in Development:
dotnet run --project Vortex.Main/Vortex.Main.csproj
Plugin loading supports both the runtime plugin folder and dev-specific paths:
- Default folder:
<runtime>/plugins - Optional config:
Vortex:Plugin:DevPluginPaths
Example:
{
"Vortex": {
"Plugin": {
"DevPluginPaths": [
"C:/Users/you/RiderProjects/turbo-sample-plugin/TurboSamplePlugin/bin/Debug/net10.0"
]
}
}
}If the same plugin key exists in both places, DevPluginPaths wins and a warning is logged.
Your plugin's .csproj must copy manifest.json to the build output:
<ItemGroup>
<Content Include="manifest.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
</ItemGroup>You can list multiple plugin paths in DevPluginPaths when developing several plugins at once.
Terminal 1 (run emulator):
dotnet run --project Vortex.MainTerminal 2 (watch plugin):
cd C:/path/to/your-plugin
dotnet watch buildWhen dotnet watch rebuilds your plugin, Vortex Cloud detects the new DLL and hot-reloads the plugin in-process.
- Grain types cannot be hot-reloaded because Orleans grain type registration happens at silo startup.
- Memory may grow over many reloads if assembly references are retained; restart periodically during long sessions.
Canonical integrated workflow lives in the plugin repo:
- Guide:
../turbo-sample-plugin/README.md - PowerShell:
pwsh -File ../turbo-sample-plugin/scripts/dev-integrated.ps1 - bash/zsh:
sh ../turbo-sample-plugin/scripts/dev-integrated.sh
Vortex Cloud uses Orleans as its core runtime model for stateful domain workflows.
For project-specific Orleans guidance, see docs/orleans.md.
For a concrete end-to-end trace of a single packet through handlers, grains, and streams, see docs/walkthroughs/request-lifecycle.md.
If you see Unable to connect to any of the specified MySQL hosts:
- Verify
Vortex:Database:ConnectionStringinappsettings.Development.json. - Verify MySQL host/port are reachable.
- Verify no
VORTEX__...environment variables override your local setting.
- Ensure
DOTNET_ENVIRONMENT=Developmentwhen running. - Confirm
appsettings.Development.jsonexists at repo root.
- Run
dotnet tool restore. - Run
dotnet csharpier .. - Run
dotnet format style. - Run
dotnet format analyzers. - Re-run
dotnet build Vortex.Main/Vortex.Main.csproj -t:VortexCloudQualityGate.
If dotnet build Vortex.Cloud.sln fails because of plugin project state, use the default core build command:
dotnet build Vortex.Main/Vortex.Main.csprojCanonical AI context files:
AGENTS.md(coding contract and review rules)CONTEXT.md(architecture boundaries and placement rules)docs/patterns/(golden implementation examples)docs/walkthroughs/request-lifecycle.md(one packet, socket to client — the real flow)docs/walkthroughs/add-a-feature.md(adding a feature, layer by layer)docs/walkthroughs/add-a-dashboard-page.md(dashboard admin surface — including the six files a capability string lives in)docs/patterns/vertical-slice.md(handler + grain + test on a single feature)docs/glossary.md(Habbo + Orleans terminology, each term mapped to its file)
Planning & design references:
ROADMAP.md(completion plan: epics → stories → Definition of Done)DATA-MODEL.md(authoritative schema + naming conventions; note that groups, rentable space and pets are implemented, not pending)PETS-DESIGN.md(pet implementation: autonomous-agent behavior, state machine, persistence)
Tool-specific adapters:
.github/copilot-instructions.md(GitHub Copilot)CLAUDE.md(Claude)CODEX.md(Codex)
Prompt recipe for any AI tool:
- Include task + exact target file paths.
- Attach
AGENTS.mdandCONTEXT.md. - Reference one relevant file from
docs/patterns/or a walkthrough fromdocs/walkthroughs/(and the relevantDATA-MODEL.mdsection for schema work). - Ask for edge-case handling and validation commands.
Boost-style prompting pack:
- portable prompt contract + task recipes live in
AGENTS.md - architecture invariants live in
CONTEXT.md - tool adapters live in
.github/copilot-instructions.md,CLAUDE.md, andCODEX.md