This software's objective is to serve as a collaborative platform for job applications with an API first design. This means it should be easy for people to extend it and integrate in to much broader workflows. I built this first of all so that I could collaborate with AI agents in my Job Application pipeline thus offering a playground for hybrid AI + Human work.
This section defines the conceptual model and core entities. Design goals: multi‑tenant collaboration, AI + human augmentation, clear provenance, and extensibility for ingestion pipelines.
Two visibility classes:
- Public (globally visible): Ingested
Companyand publicJobPostingrecords. - Organization‑scoped: User‑created
JobPostings,Applications,Conversations,Attachments, and internal enrichment artifacts. Every org‑scoped entity carriesOrganizationId.
Users may belong to multiple organizations via membership records. Authorization will typically enforce that a user can only mutate entities within orgs they are a member of (except public read). Future: row‑level policies / ABAC.
Represents an externally advertised role or an internally tracked opportunity. Fields:
- Id (PK)
- OrganizationId (nullable for public / ingested postings)
- CompanyId (FK Company)
- Title
- Description (Markdown, with optional LanguageCode)
- ApplicationProcedure: { Link (URL), Type (enum ApplicationType) }
- Location (freeform now; future: structured geolocation)
- WorkloadFte (decimal 0–1; derived from “Workload”)
- ContractType (enum)
- SpokenLanguageRequirements [LanguageRequirement]
- PublishedAt (UTC)
- SalaryRange { Min, Max, Currency (ISO 4217), Period = Annual }
- ExternalIds [ExternalId]
- CreatedAt / UpdatedAt
- ArchivedAt (nullable)
Relationships:
- Company (M:1)
- Applications (1:M)
Fields:
- Id
- Name
- Description
- Location (freeform)
- EmployeeCount (nullable)
- ExternalIds [ExternalId]
- CreatedAt / UpdatedAt
- ArchivedAt
Relationships:
- JobPostings (1:M)
- Contacts (1:M)
Represents a person associated with a single Company (simplified; no historical moves yet). Fields: Id, CompanyId, FirstName, LastName, Email, Phone, RoleTitle, CreatedAt, UpdatedAt, ArchivedAt.
Can be Human or AI agent. Fields: Id, DisplayName, Type (Human|AI), CreatedAt, UpdatedAt, Active, ArchivedAt.
Fields: Id, Name, CreatedAt, ArchivedAt.
Junction for many‑to‑many membership. Fields: UserId, OrganizationId, Role (Owner|Member|Automation), JoinedAt, ArchivedAt.
Represents one submission attempt for a JobPosting by a User. Multiple Applications per JobPosting & User are allowed (e.g., spontaneous + tailored follow‑up). Includes assignable ownership within the org for workflow. Fields:
- Id
- OrganizationId (FK)
- JobPostingId (FK)
- ApplicantUserId (FK User)
- Title (e.g., “Spontaneous – May 2025”)
- Status (enum ApplicationStatus)
- StatusChangedAt
- AssignedToUserId (nullable; current handler – AI or human)
- SubmittedAt (nullable until actually sent)
- ExternalIds [ExternalId]
- CreatedAt / UpdatedAt / ArchivedAt
Relationships:
- JobPosting (M:1)
- ApplicantUser (M:1 User)
- Questions (1:M ApplicationQuestion)
- Attachments (M:N via AttachmentLink)
- StatusHistory (1:M ApplicationStatusEvent)
- Conversations (1:M Conversation)
Stores structured Q&A prompts. Fields: Id, ApplicationId, Prompt, Answer, Source (Form|Manual|AI), OrderIndex, CreatedAt, UpdatedAt.
Generic file / document reference. Fields: Id, OrganizationId (nullable for public), Uri, Type (enum AttachmentType), MimeType, Hash (integrity), CreatedAt, UploadedByUserId (nullable if ingested), GeneratedBy (Human|AI), ArchivedAt.
Associates Attachments to parent entities (e.g., Application, JobPosting) without duplicating rows. Fields: Id, AttachmentId, EntityType, EntityId, CreatedAt.
Unified channel for communications & interviews (email threads, chat, phone notes, in‑person interview transcripts). Fields:
- Id
- OrganizationId
- ApplicationId (FK)
- Type (enum ConversationType) e.g., General, Interview, OfferNegotiation
- Channel (enum Channel) e.g., Email, LinkedIn, PhoneCall, InPerson, Chat
- StartedAt, ClosedAt (nullable)
- CreatedAt / UpdatedAt / ArchivedAt
Fields: Id, ConversationId, FromUserId (nullable), FromContactId (nullable), SentAt, Content (Markdown/plain), GeneratedBy (Human|AI), ConfidenceScore (nullable), Provenance (JSON), CreatedAt.
Immutable audit of status transitions. Fields: Id, ApplicationId, FromStatus, ToStatus, ChangedAt, ChangedByUserId (nullable if AI), Reason (nullable), Metadata (JSON).
Generic audit events beyond status: Created, Updated, Archived, AttachmentAdded, AssignmentChanged, AIActionPerformed. Fields: Id, EntityType, EntityId, EventType, ActorUserId (nullable), OccurredAt, Metadata (JSON), CorrelationId.
Represents a container for interview questions and answers, providing context about the interview scenario. Used by AI agents (like CandidateRepresenter.Bot) to access a knowledge base of pre-answered questions. Fields:
- Id
- OrganizationId (FK Organization) - organization-level scoping for shared Q&A banks
- JobPostingId (nullable FK) - optional link to specific job posting for context
- CompanyId (nullable FK) - optional link to company for context
- JobApplicationId (nullable FK) - optional link to actual application (enables tracking bot-generated answers)
- InterviewType (enum) - Simulated or Real
- Notes (optional)
- CreatedAt / UpdatedAt / ArchivedAt
Relationships:
- Questions (1:M InterviewQuestion)
- JobPosting (M:1, optional)
- Company (M:1, optional)
- JobApplication (M:1, optional)
Individual question-answer pairs within an InterviewContext. Tracks provenance and confidence for AI-generated content. Fields:
- Id
- InterviewContextId (FK)
- Question (max 1000 chars)
- Answer (max 4000 chars)
- GeneratedBy (enum) - Human or AI
- ConfidenceScore (nullable decimal 0-1) - for AI-generated answers
- Notes (optional)
- CreatedAt / UpdatedAt / ArchivedAt
Methods:
- IsAIGenerated() - convenience method to check if AI-generated
- HasHighConfidence() - returns true if confidence score >= 0.8
ExternalId { System, Value } LanguageRequirement { LanguageCode (ISO 639‑1), Level (enum LanguageLevel) } SalaryRange { Min, Max, Currency, Period }
- ApplicationType: LinkedInEasyApply, ExternalPortal, InternalATS, GenericForm
- ContractType: Permanent, Internship, Contractor, FixedTerm
- ApplicationStatus: Draft, Preparing, Submitted, Screening, Interviewing, Offer, Hired, Rejected, Withdrawn
- ConversationType: General, Interview, OfferNegotiation, FollowUp
- Channel: Email, LinkedIn, PhoneCall, InPerson, Chat, System
- AttachmentType: Resume, CoverLetter, Portfolio, Transcript, Other
- LanguageLevel: Basic, Conversational, Professional, Native
- GeneratedBy: Human, AI
- InterviewType: Simulated, Real
Company (1) ── (M) JobPosting Company (1) ── (M) Contact JobPosting (1) ── (M) Application Application (1) ── (M) ApplicationQuestion Application (1) ── (M) Conversation Conversation (1) ── (M) Message Application (1) ── (M) ApplicationStatusEvent User (M) ── (M) Organization (via UserOrganization) Entity (1) ── (M) EventLog Attachment (M) ── (M) (Entities) via AttachmentLink InterviewContext (1) ── (M) InterviewQuestion InterviewContext (M) ── (1) Organization InterviewContext (M) ── (1) JobPosting (optional) InterviewContext (M) ── (1) Company (optional) InterviewContext (M) ── (1) JobApplication (optional)
All mutable entities include CreatedAt, UpdatedAt, and optional ArchivedAt for soft deletion. State transitions for Applications are immutable via ApplicationStatusEvent. EventLog provides extensible auditing and correlation for multi‑step AI + human workflows. AI provenance captured through GeneratedBy + optional ConfidenceScore + Metadata.
- Messages, Attachments, Answers can be AI generated; provenance stored.
- Assignment model supports routing: initial AI drafting then human submission.
- Future: scoring agents can enrich Applications (stored as separate enrichment events rather than mutating base rows).
- High write volume expected in Messages & EventLog → index (ConversationId, SentAt) and (EntityType, EntityId, OccurredAt).
- Attachments stored out‑of‑band (object storage) addressed by immutable Uri + Hash.
- Status transitions append‑only → suitable for event sourcing of Application aggregate.
- Privacy & PII handling (encryption at rest for Contact + Message content if required).
- Consent / lawful basis tracking for Contact data.
- Company & JobPosting deduplication handled upstream by ingestion pipeline (not in this service).
- Localization of descriptions; fallback strategy.
- Search indexing model (e.g., projection tables or external search service).
- Role‑based access control granularity beyond membership Role.
The model favors normalization for auditability (status events, messages) while keeping value objects (language requirements, external IDs) inline for developer ergonomics. Conversations unify communications and interviews, reducing surface area. Public vs org‑scoped split enables shared ingestion while preserving collaborative privacy.
- .NET 8.0 SDK
- Clone the repository
- Navigate to the project directory
- Run
dotnet restoreto restore dependencies - Run
dotnet run --project JobTrackerto start the API - The API will be available at
https://localhost:7226(HTTPS) orhttp://localhost:5138(HTTP)
The project uses Swashbuckle for OpenAPI documentation generation and SwaggerUI hosting:
- Swagger UI: Available at
/swaggerwhen running in Development mode - OpenAPI JSON: Available at
/swagger/v1/swagger.json - Runtime Generation: OpenAPI specification is automatically generated at runtime
- Interactive API documentation with SwaggerUI
- Automatic OpenAPI 3.0 specification generation
- Clean and reliable documentation generation
- Comprehensive endpoint documentation with request/response schemas
- Try-it-out functionality for testing endpoints directly from the UI
When running the application in Development mode:
- Start the application:
dotnet run --project JobTracker - Open your browser to:
http://localhost:5138/swagger - Explore and test the API endpoints interactively
The project now includes comprehensive debugging support with the following configurations:
- Launch JobTracker API - Starts the API with automatic browser opening
- Launch JobTracker API (No Browser) - Starts the API without opening browser
- Attach to JobTracker - Attaches debugger to running process
- Debug Tests - Runs unit tests with debugging support
- Full breakpoint support with IntelliSense
- Inline variable inspection
- Debug console with auto-completion
- Automatic build before debugging
- Environment variable configuration
- Test debugging capabilities
- Open the project in VS Code
- Press
F5or go to Run and Debug panel - Select "Launch JobTracker API" configuration
- Set breakpoints in your code
- The API will start and browser will open automatically
Available via Command Palette (Ctrl+Shift+P) → "Tasks: Run Task":
- build - Build the project
- test - Run all tests
- watch - Run with file watching
- clean - Clean build artifacts
- restore - Restore NuGet packages
# Run all tests
dotnet test
# Run tests with detailed output
dotnet test --logger "console;verbosity=detailed"
# Run tests in specific project
dotnet test tests/JobTracker.Tests/JobTracker.Tests.csprojThe project uses dotnet-outdated to keep dependencies current and maintain security. This tool helps identify and update NuGet packages to their latest versions.
# Install the global tool (one-time setup)
dotnet tool install --global dotnet-outdated-tool# Keep the tool itself updated
dotnet tool update --global dotnet-outdated-tool# Check for outdated packages and update them automatically
dotnet outdated -u
# Just check without updating (dry run)
dotnet outdated
# Update packages in specific project
dotnet outdated -u --project JobTracker/JobTracker.csproj
# Update test project dependencies
dotnet outdated -u --project tests/JobTracker.Tests/JobTracker.Tests.csproj- Run
dotnet outdated -uregularly to keep dependencies current - Always test thoroughly after dependency updates
- Review breaking changes for major version updates
- Consider running updates on a separate branch for larger projects
The project uses Entity Framework Core for data persistence. When making changes to the database schema (entities, configurations, etc.), you must create and apply migrations.
# Create a new migration after modifying entities or configurations
dotnet ef migrations add <MigrationName> --project JobTracker
# Examples:
dotnet ef migrations add AddApplicationStatusEvents --project JobTracker
dotnet ef migrations add UpdateUserEntity --project JobTracker
dotnet ef migrations add AddConversationSupport --project JobTracker# Apply pending migrations to the database
dotnet ef database update --project JobTracker
# Apply migrations up to a specific migration
dotnet ef database update <MigrationName> --project JobTracker- Always create a migration when you modify:
- Entity classes in
Domain/Entities/ - Entity configurations in
Persistence/Configurations/ - DbContext or DbSet declarations
- Entity classes in
- Use descriptive migration names that clearly indicate what changed
- Review the generated migration code before applying it
- Test migrations on a copy of production data before deploying
- Never modify existing migration files; create new ones for changes
- Consider data migration scripts for complex schema changes that affect existing data
# Remove the last migration (if not yet applied to database)
dotnet ef migrations remove --project JobTracker
# List all migrations and their status
dotnet ef migrations list --project JobTracker
# Generate SQL script for migrations
dotnet ef migrations script --project JobTracker
# Generate SQL script for specific migration range
dotnet ef migrations script <FromMigration> <ToMigration> --project JobTracker- Client code generation through Orval
- Simple frontend based on that client code
- Simple Ingestor for the JobUp platform
- Persitance through Postgres EF connection
- Assigning Applications to users
- Refactor the frontend to deal with job postings and applications seperatly as was planned in the original specification
- Selection User bot creation