diff --git a/src/backend/Host/Host.csproj b/src/backend/Host/Host.csproj
new file mode 100644
index 0000000..0a693e5
--- /dev/null
+++ b/src/backend/Host/Host.csproj
@@ -0,0 +1,29 @@
+
+
+
+ net10.0
+ enable
+ enable
+
+
+
+
+
+
+
+
+
+
+
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+ all
+
+
+
+
+
+
+
+
+
+
diff --git a/src/backend/Host/Program.cs b/src/backend/Host/Program.cs
new file mode 100644
index 0000000..bbc5ea3
--- /dev/null
+++ b/src/backend/Host/Program.cs
@@ -0,0 +1,271 @@
+using Adapters.Driven.Persistence;
+using Adapters.Driven.Persistence.Context;
+using Adapters.Driving.Api.Extensions;
+using Application;
+using dotenv.net;
+using Microsoft.AspNetCore.Diagnostics.HealthChecks;
+using Microsoft.AspNetCore.RateLimiting;
+using Microsoft.AspNetCore.ResponseCompression;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.Extensions.Diagnostics.HealthChecks;
+using Serilog;
+using System.IO.Compression;
+using System.Text.Json;
+using System.Threading.RateLimiting;
+
+// Load .env file (searches up to root directory)
+DotEnv.Load(options: new DotEnvOptions(
+ probeForEnv: true,
+ probeLevelsToSearch: 5
+));
+
+// Configure Serilog from appsettings + environment variables
+Log.Logger = new LoggerConfiguration()
+ .ReadFrom.Configuration(new ConfigurationBuilder()
+ .AddJsonFile("appsettings.json")
+ .AddJsonFile($"appsettings.{Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT") ?? "Production"}.json", optional: true)
+ .AddEnvironmentVariables()
+ .Build())
+ .CreateLogger();
+
+try
+{
+ Log.Information("Starting application");
+
+ var builder = WebApplication.CreateBuilder(args);
+
+ // Add environment variables to configuration
+ builder.Configuration.AddEnvironmentVariables();
+
+ // Build connection string from individual env vars if not provided directly
+ var connectionString = builder.Configuration.GetConnectionString("DefaultConnection");
+ if (string.IsNullOrEmpty(connectionString))
+ {
+ var dbHost = Environment.GetEnvironmentVariable("DB_HOST") ?? "localhost";
+ var dbPort = Environment.GetEnvironmentVariable("DB_PORT") ?? "1433";
+ var dbName = Environment.GetEnvironmentVariable("DB_NAME") ?? "StudentEnrollment";
+ var dbUser = Environment.GetEnvironmentVariable("DB_USER") ?? "sa";
+ var dbPassword = Environment.GetEnvironmentVariable("DB_PASSWORD") ?? "";
+
+ connectionString = $"Server={dbHost},{dbPort};Database={dbName};User Id={dbUser};Password={dbPassword};TrustServerCertificate=True";
+ builder.Configuration["ConnectionStrings:DefaultConnection"] = connectionString;
+ }
+
+ builder.Host.UseSerilog();
+
+ // Add services
+ builder.Services.AddApplication();
+ builder.Services.AddPersistence(builder.Configuration);
+ builder.Services.AddGraphQLApi();
+
+ // Response Compression (Brotli preferred, then Gzip)
+ builder.Services.AddResponseCompression(options =>
+ {
+ options.EnableForHttps = true;
+ options.Providers.Add();
+ options.Providers.Add();
+ options.MimeTypes = ResponseCompressionDefaults.MimeTypes.Concat(
+ ["application/json", "application/graphql-response+json"]);
+ });
+
+ builder.Services.Configure(options =>
+ options.Level = CompressionLevel.Fastest);
+
+ builder.Services.Configure(options =>
+ options.Level = CompressionLevel.Fastest);
+
+ // CORS (configurable via CORS_ORIGINS env var, comma-separated)
+ var corsOrigins = Environment.GetEnvironmentVariable("CORS_ORIGINS")?.Split(',', StringSplitOptions.RemoveEmptyEntries)
+ ?? ["http://localhost:4200"];
+ builder.Services.AddCors(options =>
+ {
+ options.AddDefaultPolicy(policy =>
+ policy.WithOrigins(corsOrigins)
+ .AllowAnyHeader()
+ .AllowAnyMethod());
+ });
+
+ // Output caching for read-heavy operations
+ builder.Services.AddOutputCache(options =>
+ {
+ options.AddBasePolicy(policy => policy.Expire(TimeSpan.FromSeconds(10)));
+ options.AddPolicy("Subjects", policy => policy.Expire(TimeSpan.FromMinutes(5)));
+ options.AddPolicy("Professors", policy => policy.Expire(TimeSpan.FromMinutes(5)));
+ });
+
+ // Health checks
+ builder.Services.AddHealthChecks()
+ .AddDbContextCheck("database");
+
+ // Rate limiting (DoS prevention)
+ builder.Services.AddRateLimiter(options =>
+ {
+ options.RejectionStatusCode = StatusCodes.Status429TooManyRequests;
+ options.AddFixedWindowLimiter("graphql", limiter =>
+ {
+ limiter.PermitLimit = 100;
+ limiter.Window = TimeSpan.FromMinutes(1);
+ limiter.QueueProcessingOrder = QueueProcessingOrder.OldestFirst;
+ limiter.QueueLimit = 10;
+ });
+ options.AddFixedWindowLimiter("mutations", limiter =>
+ {
+ limiter.PermitLimit = 30;
+ limiter.Window = TimeSpan.FromMinutes(1);
+ limiter.QueueLimit = 5;
+ });
+ });
+
+ var app = builder.Build();
+
+ // Verify database connection and apply migrations
+ await VerifyDatabaseConnectionAsync(app);
+
+ async Task VerifyDatabaseConnectionAsync(WebApplication app)
+ {
+ var maxRetries = 10;
+ var delay = TimeSpan.FromSeconds(5);
+
+ for (int attempt = 1; attempt <= maxRetries; attempt++)
+ {
+ try
+ {
+ using var scope = app.Services.CreateScope();
+ var db = scope.ServiceProvider.GetRequiredService();
+
+ Log.Information("Attempting database connection (attempt {Attempt}/{MaxRetries})...", attempt, maxRetries);
+
+ // Try to apply migrations - this will create the DB if it doesn't exist
+ Log.Information("Applying database migrations (this will create DB if needed)...");
+ await db.Database.MigrateAsync();
+ Log.Information("Database migrations applied successfully");
+
+ // Verify connection works after migrations
+ if (await db.Database.CanConnectAsync())
+ {
+ Log.Information("Database connection verified successfully");
+ return;
+ }
+ }
+ catch (Exception ex)
+ {
+ Log.Error(ex, "Database operation failed (attempt {Attempt}/{MaxRetries}): {ErrorMessage}",
+ attempt, maxRetries, ex.Message);
+
+ if (attempt == maxRetries)
+ {
+ Log.Fatal("Could not establish database connection after {MaxRetries} attempts. " +
+ "Please verify: DB_HOST={DbHost}, DB_PORT={DbPort}, DB_NAME={DbName}, DB_USER={DbUser}",
+ maxRetries,
+ Environment.GetEnvironmentVariable("DB_HOST") ?? "localhost",
+ Environment.GetEnvironmentVariable("DB_PORT") ?? "1433",
+ Environment.GetEnvironmentVariable("DB_NAME") ?? "StudentEnrollment",
+ Environment.GetEnvironmentVariable("DB_USER") ?? "sa");
+ }
+ }
+
+ if (attempt < maxRetries)
+ {
+ Log.Information("Waiting {Delay} seconds before next connection attempt...", delay.TotalSeconds);
+ await Task.Delay(delay);
+ }
+ }
+ }
+
+ // Security headers (OWASP recommended)
+ app.Use(async (context, next) =>
+ {
+ var headers = context.Response.Headers;
+ headers.Append("X-Content-Type-Options", "nosniff");
+ headers.Append("X-Frame-Options", "DENY");
+ headers.Append("X-XSS-Protection", "0");
+ headers.Append("Referrer-Policy", "strict-origin-when-cross-origin");
+ headers.Append("Permissions-Policy", "geolocation=(), microphone=(), camera=()");
+ // CSP: More restrictive in production, relaxed for dev (GraphQL Playground needs unsafe-inline/eval)
+ var csp = app.Environment.IsDevelopment()
+ ? "default-src 'self'; " +
+ "script-src 'self' 'unsafe-inline' 'unsafe-eval'; " +
+ "style-src 'self' 'unsafe-inline'; " +
+ "img-src 'self' data: https:; " +
+ "font-src 'self'; " +
+ "connect-src 'self' http://localhost:* https://localhost:*; " +
+ "frame-ancestors 'none'; " +
+ "base-uri 'self'; " +
+ "form-action 'self'"
+ : "default-src 'self'; " +
+ "script-src 'self'; " +
+ "style-src 'self'; " +
+ "img-src 'self' data:; " +
+ "font-src 'self'; " +
+ "connect-src 'self'; " +
+ "frame-ancestors 'none'; " +
+ "base-uri 'self'; " +
+ "form-action 'self'";
+ headers.Append("Content-Security-Policy", csp);
+ if (!context.Request.IsHttps && !app.Environment.IsDevelopment())
+ {
+ headers.Append("Strict-Transport-Security", "max-age=31536000; includeSubDomains");
+ }
+ await next();
+ });
+
+ // Request logging (structured logs, excludes sensitive data)
+ app.UseSerilogRequestLogging(options =>
+ {
+ options.EnrichDiagnosticContext = (diagnosticContext, httpContext) =>
+ {
+ diagnosticContext.Set("RequestHost", httpContext.Request.Host.Value);
+ diagnosticContext.Set("UserAgent", httpContext.Request.Headers.UserAgent.ToString());
+ };
+ options.MessageTemplate = "HTTP {RequestMethod} {RequestPath} responded {StatusCode} in {Elapsed:0.0000} ms";
+ });
+
+ // Middleware order matters for performance
+ app.UseResponseCompression();
+ app.UseCors();
+ app.UseRateLimiter();
+ app.UseOutputCache();
+
+ // Health check endpoint with JSON response
+ app.MapHealthChecks("/health", new HealthCheckOptions
+ {
+ ResponseWriter = async (context, report) =>
+ {
+ context.Response.ContentType = "application/json";
+ var result = new
+ {
+ status = report.Status.ToString(),
+ timestamp = DateTime.UtcNow,
+ checks = report.Entries.Select(e => new
+ {
+ name = e.Key,
+ status = e.Value.Status.ToString(),
+ description = e.Value.Description,
+ duration = e.Value.Duration.TotalMilliseconds
+ })
+ };
+ await context.Response.WriteAsync(JsonSerializer.Serialize(result, new JsonSerializerOptions
+ {
+ PropertyNamingPolicy = JsonNamingPolicy.CamelCase
+ }));
+ }
+ });
+
+ // GraphQL endpoint with Banana Cake Pop playground and rate limiting
+ app.MapGraphQL()
+ .RequireRateLimiting("graphql")
+ .WithOptions(new HotChocolate.AspNetCore.GraphQLServerOptions
+ {
+ Tool = { Enable = app.Environment.IsDevelopment() }
+ });
+
+ await app.RunAsync();
+}
+catch (Exception ex)
+{
+ Log.Fatal(ex, "Application terminated unexpectedly");
+}
+finally
+{
+ await Log.CloseAndFlushAsync();
+}
diff --git a/src/backend/Host/appsettings.Development.json b/src/backend/Host/appsettings.Development.json
new file mode 100644
index 0000000..c84fe47
--- /dev/null
+++ b/src/backend/Host/appsettings.Development.json
@@ -0,0 +1,5 @@
+{
+ "GraphQL": {
+ "EnableIntrospection": true
+ }
+}
diff --git a/src/backend/Host/appsettings.json b/src/backend/Host/appsettings.json
new file mode 100644
index 0000000..3f1cc23
--- /dev/null
+++ b/src/backend/Host/appsettings.json
@@ -0,0 +1,55 @@
+{
+ "ConnectionStrings": {
+ "DefaultConnection": ""
+ },
+ "Serilog": {
+ "Using": ["Serilog.Sinks.Console", "Serilog.Sinks.File"],
+ "MinimumLevel": {
+ "Default": "Information",
+ "Override": {
+ "Microsoft": "Warning",
+ "Microsoft.AspNetCore": "Warning",
+ "Microsoft.EntityFrameworkCore": "Warning",
+ "HotChocolate": "Warning",
+ "System": "Warning"
+ }
+ },
+ "WriteTo": [
+ {
+ "Name": "Console",
+ "Args": {
+ "outputTemplate": "[{Timestamp:HH:mm:ss} {Level:u3}] {Message:lj} {Properties:j}{NewLine}{Exception}"
+ }
+ },
+ {
+ "Name": "File",
+ "Args": {
+ "path": "logs/app-.log",
+ "rollingInterval": "Day",
+ "retainedFileCountLimit": 7,
+ "outputTemplate": "{Timestamp:yyyy-MM-dd HH:mm:ss.fff zzz} [{Level:u3}] {Message:lj} {Properties:j}{NewLine}{Exception}"
+ }
+ }
+ ],
+ "Enrich": ["FromLogContext", "WithMachineName", "WithThreadId"],
+ "Destructure": [
+ {
+ "Name": "ToMaximumDepth",
+ "Args": { "maximumDestructuringDepth": 3 }
+ }
+ ],
+ "Filter": [
+ {
+ "Name": "ByExcluding",
+ "Args": {
+ "expression": "Contains(@Message, 'password') or Contains(@Message, 'Password') or Contains(@Message, 'secret') or Contains(@Message, 'token') or Contains(@Message, 'connectionString')"
+ }
+ }
+ ]
+ },
+ "AllowedHosts": "*",
+ "GraphQL": {
+ "MaxExecutionDepth": 5,
+ "MaxComplexity": 100
+ }
+}