Please note: This article is OTYS only, and can only be read by users that have logged in.
Reminder for developers
Before working on this project in any way or form, make sure to fully understand what it is, how it’s setup and what roles there are. This information can be found in other articles.
Tech-stack used
The Corporate portal consists of 2 main projects, the Front-End and Back-end. The Front-end is an Angular v21 applications and the Back-end an Symfony 7.4 application.
Below the 3th party packages used that are not directly bound to either Angular or Symfony.
Project | Package | Description |
|---|---|---|
Front-end | Angular CDK | Used for drag&drop and more |
Front-end | Transloco | Used for translations |
Front-end | Splide JS | Used for sliding animations |
Front-end | Angular Calendar | Used for calendar features |
Front-end | AngularX QR Code | Used for QR code generation |
Front-end | Date FNS | Date formatting |
Front-end | NGX Editor | WYSIWYG Editor |
Front-end | Vanilla Colorful | Color picker |
Back-end | API Platform | REST API |
Back-end | AWS SDK | Cloudflare integration |
Back-end | Firebase PHP JWT | JWT token generation |
Back-end | Gedmo | Translations |
Back-end | Nelmio Cors | CORS Policy |
OWS Usage
This application directly connects to OWS. Most requests are made on behalf of the user, and their session id. However some requests are done with our Your!T api key. Below a list per type:
User Requests
Otys.Services.VacancyService.getDetail> Get vacancy detailOtys.Services.VacancyService.getListEx> List vacanciesOtys.Services.VacancyService.createFromProfile> Create vacancy from templateOtys.Services.VacancyService.update> Update vacancyOtys.Services.VacancyService.Update> Update vacancy fields (batch)Otys.Services.VacancyService.updateEx> Update vacancy (extended)Otys.Services.VacancyService.delete> Delete vacancyOtys.Services.VacancyService.getExtraFieldsList> Get vacancy extra fieldsOtys.Services.VacancyService.getExtraFieldOptionsList> Get options for extra fieldOtys.Services.VacancyService.getVancancyProfileList> List vacancy templatesOtys.Services.VacancyService.getVacancyProfile> Get vacancy profileOtys.Services.VacancyService.getOptionLists> Get option lists (HM/VM/CM)Otys.Services.VacancyService.getSearchFacilityInfo> Get Actonomy search infoOtys.Services.VacancyStatusService.getList> List vacancy statusesOtys.Services.VacancyTypeService.getList> List vacancy typesOtys.Services.VacancyQuestionSetService.getVacancyQuestionSet> Get vacancy question setOtys.Services.VacancyQuestionSetService.getDetail> Get question set detailOtys.Services.ProcedureService.getDetail> Get procedure detailOtys.Services.ProcedureService.getListEx> List proceduresOtys.Services.ProcedureService.update> Update procedureOtys.Services.ProcedureStatus1Service.getList> List procedure statusesOtys.Services.ProcedureRejectionReasonsSettingsService.getList> List rejection reasonsOtys.Services.CandidateService.getDetail> Get candidate detailOtys.Services.CandidateService.getListEx> List candidatesOtys.Services.CandidateService.getOptionLists> Get candidate option lists (translations)Otys.Services.UserService.getDetail> Get user profile detailOtys.Services.UserService.getListEx> List usersOtys.Services.UserService.update> Update user profileOtys.Services.SettingsService.get> Get settings (locale, appointment planner)Otys.Services.MatchCriteriaSettingsService.getList> List match criteria settingsOtys.Services.FileService.getTempKey> Get temp download key for documentOtys.Services.FileService.bind> Bind uploaded file to entityOtys.Services.AttachedDocumentsService.getList> List attached documentsOtys.Services.NoticeService.getTypesForEntity> Get notice types for entityOtys.Services.NoticeService.add> Add a notice/noteOtys.Services.NoticeService.getDetail> Get notice detailOtys.Services.DocumentTypeService.add> Add document typeOtys.Services.DossierService.getList> List dossier entriesOtys.Services.EmailService.send> Send emailOtys.Services.CalendarRemoteService.getAppointmentsTypes> Get appointment typesOtys.Services.CalendarRemoteService.getColleagues> Get colleaguesOtys.Services.CalendarRemoteService.getAppointments> Get calendar appointmentsOtys.Services.CalendarRemoteService.putAppointment> Create appointmentOtys.Services.CaldavService.getOutlookRefreshToken> Get Outlook refresh tokenupdatePassword> Change password (authenticated)logout> Logout current sessionuserFileUploadRequest> Upload file to OWS
App Requests (your!t)
Otys.Services.CsmService.getValue> Get CSM config value (roles, TOTP enabled)Otys.Services.UserService.getClientUsers> Find user by username for password resetOtys.Services.CsmService.setValue> Set temporary password via CSMOtys.Services.UserService.getTotpSettings> Get TOTP setup (secret + QR)Otys.Services.UserService.verifyTotpCode> Verify TOTP code during setupOtys.Services.WebsiteService.getList> Sync client brands/websites
Session-less Requests (based around temp user session)
login> Login (obtain OWS session)check> Validate existing sessionOtys.Services.UserService.getDetail> Get user detail during auth flowlogout> Logout temp session after password resetlogOutDevice> Logout a specific device sessioncheckTotpcode> Verify TOTP code (pre-auth)updatePassword> Update password with temp session
File Requests (user based)
VacancyPhoto> Get vacancy photoUserPhoto> Get user photoCandidatePhoto> Get candidate photo
Back-end Caching setup
The back-end cache is setup for OWS caching
The central caching mechanism for OWS data is OwsCacheService. This service wraps Symfony's tag-aware cache and adds multi-tenancy awareness. It exposes the following key method:
getOrCompute(string $key, callable $callback, int $ttl, array $tags, ?int $clientId): Attempts to retrieve a cached value by key. If the key is not found, it executes the callback, caches the result with the given TTL and tags, and returns it.
Cache keys are automatically scoped to the current tenant. The service prepends ows_{clientId}_ to every key, so a key like vacancy_detail_abc123 for client 42 becomes ows_42_vacancy_detail_abc123 (before Redis-level prefixing). When no user is authenticated (e.g. during background sync), the client ID can be passed explicitly.
Automatic Tagging
Every cached item receives three automatic tags:
client_{clientId} — groups all cache entries for a specific tenant.
The "category" tag — derived from the first segment of the key (e.g. vacancy from vacancy_detail_123, procedure from procedure_statuses).
client_{clientId}_{category} — combines both for client-scoped category invalidation.
Additional tags can be passed via the $tags parameter for fine-grained invalidation (e.g. vacancy_123 to tag a specific vacancy's cache entries).
TTLs in Practice
Different data types use different TTLs based on how frequently they change:
10 seconds: Vacancy detail (changes frequently during editing)
600 seconds (10 min): Client brand websites
3600 seconds (1 hour): Default for most data
86400 seconds (24 hours): Relatively static data like procedure statuses, vacancy question sets, and rejection reasons
Cache Invalidation
Cache invalidation is tag-based, with three strategies:
By entity type (invalidateByEntityType): Invalidates the client_{clientId}_{entityType} tag, clearing all cached data of that type for the current tenant. Used after bulk operations or when multiple entries of a type may be stale (e.g. invalidating all procedure cache after a status change).
By specific entity (invalidateEntity): Invalidates a specific tag like vacancy_{vacancyUid}, clearing only the cache entries tagged with that specific entity ID. Used after updates to individual resources.
Full client cache clear (clearClientCache): Invalidates the client_{clientId} tag, clearing all OWS cache for the current tenant. Exposed via DELETE /api/cache for Portal Admins.
Front-end caching setup
The application uses a custom HTTP caching layer built on an Angular HttpInterceptorFn. It caches GET request responses in localStorage, scoped per user, with configurable TTLs and multiple caching strategies. Only GET requests are cached; mutations (POST, PUT, PATCH, DELETE) bypass caching and automatically invalidate related cache entries.
Storage
Cached responses are stored in localStorage. Each entry is keyed with a prefix that includes an environment-specific cache prefix and the current user's ID (e.g., cp_user_42_/api/vacancies?page=1). This ensures cached data is isolated per user and per environment. Entries are stored as JSON objects containing the response data, a timestamp, and the TTL.
For content-language-aware endpoints, the Accept-Language header value is appended to the cache key (e.g., /api/vacancies|lang=nl), so responses in different languages are cached separately.
Cache Strategies
Each HTTP GET request can specify one of four strategies via an Angular HttpContextToken:
cache-first: Checks localStorage first. If a valid (non-expired) entry exists, it is returned immediately without making a network request. If not, the request is made, and the response is cached for future use.
network-first: Always makes a network request. On success, the response is cached. On failure (network error), falls back to the cached entry if one exists. Useful for data where freshness is important but offline resilience is desired.
cache-then-network (default for most entity reads): Returns the cached entry immediately if available, then fires a network request in the background. The subscriber receives two emissions: first the cached data, then the fresh network data. If the network request fails after cache was served, a stale-data warning toast is shown to the user (unless the error is a 404). This gives the user instant perceived performance while still keeping data fresh.
network-only: Bypasses caching entirely. The request always goes to the network, and the response is not stored. Used for mutation-adjacent reads (e.g., refresh()) and endpoints where caching is inappropriate.
Request Deduplication
An in-memory Map tracks in-flight requests by cache key. If a second request is made for the same resource while the first is still pending, the second subscriber shares the same Observable (via RxJS share()). This prevents duplicate simultaneous HTTP calls to the same endpoint.
TTLs
The BaseEntityService accepts a TTL in its constructor. If not specified, it defaults to 5 minutes (300,000 ms). The global fallback TTL (used when no strategy token is provided on a request) is also 5 minutes, configured via the HTTP_CACHE_CONFIG injection token.
Most entity services use a 24-hour TTL (86,400,000 ms). This applies to client, client brand, procedure, procedure status, vacancy, vacancy flow, vacancy flow step, vacancy status, vacancy template, and vacancy type services. The activity log service is the exception, using a 1-minute TTL due to its frequently changing nature.
Cache Exclusions
Auth-related endpoints (URLs matching /auth/) are excluded from caching entirely, regardless of strategy. This is configured via the excludePatterns array on the global cache config.
Cache Invalidation
On mutation: when a create, update, patch, or delete operation is performed through a BaseEntityService, all cache entries whose key contains that service's base URL are invalidated. For example, creating a vacancy removes all cached entries matching /vacancies.
On logout: the clearByUserPrefix() method removes all localStorage entries for the logged-out user's ID prefix, ensuring no cached data leaks between user sessions.
Each entity service also exposes a refresh() method that first invalidates the cache for that entity, then fetches fresh data with network-only.
Automatic Cleanup
A background timer runs every 5 minutes and scans all cache entries for the current user. Expired entries (where the elapsed time exceeds the TTL) are removed from localStorage. If localStorage runs out of space when writing a new entry, expired entries are cleared and the write is retried.
Devops setup
Unclear right now