diff --git a/.gitea/workflows/dev.yml b/.gitea/workflows/dev.yml
new file mode 100644
index 0000000..3adde84
--- /dev/null
+++ b/.gitea/workflows/dev.yml
@@ -0,0 +1,77 @@
+name: Dev Build
+
+on:
+ pull_request:
+ branches: [dev]
+ push:
+ branches: [dev]
+
+jobs:
+ build-test:
+ runs-on: ubuntu-latest
+
+ steps:
+ - uses: actions/checkout@v4
+
+ - name: Setup .NET
+ uses: actions/setup-dotnet@v4
+ with:
+ dotnet-version: '9.0.x'
+
+ - name: Restore backend
+ run: dotnet restore
+
+ - name: Build backend
+ run: dotnet build --no-restore --configuration Release
+
+ - name: Test backend
+ run: dotnet test --no-build --configuration Release
+
+ - name: Setup Node
+ uses: actions/setup-node@v4
+ with:
+ node-version: "24"
+
+ - name: Install frontend deps
+ run: npm ci
+ working-directory: ./frontend
+
+ - name: Build frontend
+ run: npm run build
+ working-directory: ./frontend
+
+ deploy-dev:
+ needs: build-test
+ if: gitea.event_name == 'push'
+ runs-on: ubuntu-latest
+
+ steps:
+ - name: Deploy dev
+ run: |
+ echo "${{ secrets.DEV_DEPLOY_SSH_KEY }}" > /tmp/dev_key
+ chmod 600 /tmp/dev_key
+ ssh -i /tmp/dev_key -o StrictHostKeyChecking=no \
+ ${{ secrets.DEV_DEPLOY_USER }}@${{ secrets.DEV_DEPLOY_HOST }} \
+ "${{ secrets.DEV_DEPLOY_PATH }}/deploy.sh"
+
+ notify-success:
+ needs: [build-test, deploy-dev]
+ if: success() && gitea.event_name == 'push'
+ runs-on: ubuntu-latest
+ steps:
+ - name: Notify Slack success
+ run: |
+ curl -X POST -H 'Content-type: application/json' \
+ --data "{\"text\":\"β
Extrudex dev deployed successfully from dev branch.\"}" \
+ "${{ secrets.SLACK_WEBHOOK_URL }}"
+
+ notify-failure:
+ needs: [build-test, deploy-dev]
+ if: failure()
+ runs-on: ubuntu-latest
+ steps:
+ - name: Notify Slack failure
+ run: |
+ curl -X POST -H 'Content-type: application/json' \
+ --data "{\"text\":\"π¨ Extrudex dev pipeline failed. Check Gitea Actions for details.\"}" \
+ "${{ secrets.SLACK_WEBHOOK_URL }}"
\ No newline at end of file
diff --git a/.gitignore b/.gitignore
index 29ec7d0..27cb172 100644
--- a/.gitignore
+++ b/.gitignore
@@ -2,4 +2,9 @@ bin/
obj/
*.user
*.suo
-.vs/
\ No newline at end of file
+.vs/
+
+# Frontend build artifacts
+frontend/dist/
+frontend/node_modules/
+frontend/.angular/
\ No newline at end of file
diff --git a/backend/.dockerignore b/backend/.dockerignore
new file mode 100644
index 0000000..c5a3aee
--- /dev/null
+++ b/backend/.dockerignore
@@ -0,0 +1,27 @@
+# Build artifacts
+bin/
+obj/
+
+# IDE / editor
+.vs/
+.vscode/
+*.user
+*.suo
+.idea/
+
+# Environment & secrets
+appsettings.Development.json
+.env
+.env.*
+
+# Docker
+Dockerfile
+.dockerignore
+
+# OS
+.DS_Store
+Thumbs.db
+
+# Misc
+*.md
+*.log
\ No newline at end of file
diff --git a/backend/API/Filters/FluentValidationFilter.cs b/backend/API/Filters/FluentValidationFilter.cs
new file mode 100644
index 0000000..58d89f0
--- /dev/null
+++ b/backend/API/Filters/FluentValidationFilter.cs
@@ -0,0 +1,69 @@
+using FluentValidation;
+using Microsoft.AspNetCore.Mvc;
+using Microsoft.AspNetCore.Mvc.Filters;
+
+namespace Extrudex.API.Filters;
+
+///
+/// Action filter that automatically validates request DTOs using FluentValidation
+/// validators registered in DI. Runs before the controller action executes.
+/// Returns 400 Bad Request with validation errors if validation fails.
+///
+public class FluentValidationFilter : IAsyncActionFilter
+{
+ private readonly IServiceProvider _serviceProvider;
+ private readonly ILogger _logger;
+
+ public FluentValidationFilter(IServiceProvider serviceProvider, ILogger logger)
+ {
+ _serviceProvider = serviceProvider;
+ _logger = logger;
+ }
+
+ public async Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next)
+ {
+ foreach (var argument in context.ActionArguments.Values)
+ {
+ if (argument is null) continue;
+
+ var argumentType = argument.GetType();
+ var validatorType = typeof(IValidator<>).MakeGenericType(argumentType);
+
+ // Try to resolve a validator for this argument type
+ var validator = _serviceProvider.GetService(validatorType) as IValidator;
+ if (validator is null) continue;
+
+ _logger.LogDebug("Validating {Type} with {Validator}", argumentType.Name, validator.GetType().Name);
+
+ var validationResult = await validator.ValidateAsync(
+ new ValidationContext