Opportunita is a platform for people applying to PhD, PostDoc and research positions. It collects opportunities from official university sources, turns each one into structured, comparable information, and carries the applicant from finding a position all the way through preparing and tracking the application.
I designed and built it: the product, the architecture, the interface, and the system that runs it in production.
The first screen states provenance before it states value. Search is written as language, not as a filter form.
An academic opportunity platform: discovery, offer intelligence, personalized recommendations, an academic resume studio, and application management, connected as one path rather than five tools.
PhD, PostDoc and research applicants. People moving between countries and university systems they have never dealt with before.
Academic opportunities live on hundreds of different university career pages, in different structures, with the important details buried. And once you find one, the work of applying starts somewhere else entirely.
I defined the problem, designed the product and its architecture, directed the interface and the brand, specified and drove the implementation, and run it in production.
A PhD position is published on the university's own career page. So is the next one, on a different university's page, in a different structure, in a different language, with the salary written as a national pay-scale code instead of a number.
Every university publishes on its own site, in its own system.
Salary, contract length, workload and deadline are written differently everywhere, and often not written at all.
Requirements sit inside a wall of prose. Nothing is filterable.
After discovery, the actual work happens in a spreadsheet, a folder, and a chat window.
The scarce resource here is not opportunities. It is comparability, and the continuity between finding something and acting on it.
The founding decision was not "build a better job board". It was: take what universities already publish, convert it into a consistent structure, explain it in plain language, and keep the official page as the authority the whole way through.
That single constraint shaped everything downstream. If the official source always wins, then the system's job is collection, normalization, classification and explanation, never substitution. It is why every opportunity carries its source domain, why the apply button leaves for the university's own site, and why the interface says "not provided by source" instead of guessing.
Every part of the platform sits on one of these four steps. Nothing was built that could not be placed on this line.
Verified opportunities from official university pages, filterable by the things that actually decide a choice: country, discipline, language, salary, deadline, position type.
Each opportunity as structured fields plus a plain-language summary, salary scheme decoding, ranking context and institutional signals.
An academic resume built as structured evidence, checked, tailored per application, and exported as a real document.
A personal workspace where each application moves stage by stage, with notes, tasks and deadlines, and applies on the university's own site.
Browse is built around the filters that actually decide an academic application: position type, country, language, university, discipline, salary range and deadline window, not around keyword search alone.
A working rebuild of the browse surface. Density is a user choice, not a designer's, and where a university published a pay-scale reference instead of a number the card says so rather than inventing a range.
The shipped screen. The counters above the filters are the honest ones, so the size of the index is never implied, it is stated.
Every card shows its source status. The full filter rail carries keyword, position type, country, language, university, disciplines, salary range and deadline window, and the density switcher changes how much comparison fits on screen without changing the query.
The difference between a job board and this platform is what happens to a posting after it is collected. Every opportunity is classified into a fixed discipline taxonomy and indexed against extracted research keywords.
The discipline directory shows every category, including the ones with zero open positions. That is deliberate: a taxonomy that hides its empty branches is a taxonomy you cannot trust. Keywords and subjects then let a researcher move sideways through the index, from one position to the adjacent field, rather than starting a new search.
Eight fixed discipline categories, each with a live count. Zero-count categories stay visible so the shape of the taxonomy stays honest.
Related paths generated from the record itself: country, institution, discipline, position type, and extracted research keywords.
The offer page answers the questions an applicant actually asks first: salary, deadline, contract length, employment type, department, as fields, at the top, before a single paragraph of the university's prose.
The Institute invites applications for a part-time research associate position with the opportunity to pursue a doctoral degree. The position is initially limited and remuneration is paid according to the applicable public-sector collective agreement, subject to the fulfilment of the personal requirements. Applications should be submitted through the online portal, quoting the reference number, and should include a letter of motivation, a curriculum vitae, copies of certificates and the names of two academic referees. The closing date for applications can be found in the header of this announcement. Working hours amount to a percentage of the regular weekly working time and may be adjusted by agreement. The Institute is committed to increasing the proportion of women in research and therefore expressly encourages qualified women to apply. Applicants with disabilities will be given preference in the case of equal suitability. Further information about the research group, the doctoral programme, the supervision arrangements and the location can be obtained from the contact person named below.
Salary, contract length, workload, deadline and employment type are all in there. None of them is a field, so none of them can be filtered, sorted or compared with the next posting.
Switch between the two views. This is the whole product in one gesture: the prose is still there, it is just no longer the only way to read the position. Where the source published nothing, the field says so instead of guessing.
Underneath sits a plain-language summary of the position, generated from the stored posting and labelled as such, with a permanent line reminding the reader to confirm details in the official posting. The apply panel names the source domain and states the verification status. The primary action leaves for the university's own site.
Trust is a UI decision, not a policy page.
Ranking references and sponsor signals carry their own disclaimer. A signal that cannot be over-read is worth more than a signal that impresses.
University ranking references, and institutional signals such as a recognised-sponsor register entry. Each one is framed as a source-backed signal, explicitly not a guarantee of admission or a visa.
Getting an opportunity onto that page is the least visible and most demanding part of the system. Each source publishes differently, so each source gets a purpose-built adapter.
An adapter finds the posting on the official career page, reads it, and maps it into the platform's internal structure. From there it is shared machinery: normalization of dates, salaries, contract terms and languages; a validation matrix that scores the result; de-duplication against source URL, external identifier and content hash; classification; then enrichment. Every run is logged and every version snapshotted, so a wrong field can be traced back to the run that produced it.
The adapters do not decide anything important. Whether an opportunity becomes public, and what happens when it disappears from the source, is decided centrally, so eighteen different adapters cannot drift into eighteen different behaviours.
Preferences are the hardest screen in the product, because every option a user sets silently changes what they will never see again. So the screen was rebuilt to say that out loud.
Write it as a sentence. Each chip carries its own rule, so there is no separate list of required and excluded values to keep in sync.
The shipped surface. The funnel is shown, not hidden, and where the source published no salary the card says not provided by source.
Bands, not numbers. Top match and strong match are orderings a person can argue with.
There is no percentage anywhere in this surface, and that is a permanent product rule rather than a gap. A number attached to a person's chances is a claim the system cannot honestly make, so it does not make it. Reasons can be argued with; a score cannot.
An academic CV is not a one-page résumé. It is a structured record of evidence (publications, grants, teaching, conferences, supervision, methods) and it changes shape for every application.
Built as data, not as a document, arranged into sections you can order and title yourself, with up to 20 tailored variants per person.
No AI, no credits. It reports what a machine reader will and will not find, and each finding carries why it matters and how to fix it.
Real page breaks, real page count, before generating. The document can also be produced in another language from the same evidence.
The preview renders the true PDF pagination, not an approximation. Getting that right is the difference between a preview and a promise.
Style studio: accent theme, typography scale and spacing density. The resume inherits the platform's design language rather than a template marketplace's.
A warning that says "an email address is saved but hidden from this export" is worth more than a score. The score is there too. It just is not the point.
The export can also be produced in another language, so the same structured evidence can address a German or Italian institution without maintaining a second CV.
Most tools in this space stop at discovery. The application itself, with its deadlines, versions, follow-ups and outcomes, is where the actual work is, and where applicants lose track.
Six columns over a nine-stage model. Accepted, rejected and offer are outcomes, not lanes of work, so they collapse into one column: a board is for work in progress, not for results. Expired and withdrawn postings stay visible and labelled rather than disappearing and taking your history with them.
The shipped board, with its honest lifecycle states.
"Do this next" is the primary object on the home screen. A workspace that opens on statistics makes you decide what to do before it helps you do it.
Everything saved, viewed or started, in one place, sorted by urgency rather than by date added.
Dark, dim and light are not three themes bolted on at the end. They are one token system resolved three ways, and the product ships all three because people read job postings at midnight and at noon.
PhD candidate in mechanism-informed world models and agents
University of Amsterdam · Netherlands
€3,059 – €3,881 gross monthly
Nothing moves. Only the canvas and surface tiers resolve differently, while the accent and the semantic colours stay constant.
The layout, density, hierarchy and component behaviour are identical across all three. The teal accent and the semantic colours (verified, closing soon, expired) stay constant, so a user who learns the colour language in one appearance keeps it in the others.
The same shipped screen in all three appearances.
The platform is not one application. It is seven purpose-built systems with explicit ownership boundaries, and the reason is simple: public opportunity data and private user data should never live in the same place.
{{ layerNote }}
Ownership is strict. The public catalog owns opportunities, institutions, countries, the taxonomy and the ingestion pipeline. The private workspace owns everything belonging to a person. The matching engine owns only derived intelligence: it never owns an input. Cross-system reads happen through published contracts with explicit allowlists, not by reaching into another system's tables, and every contract client fails closed.
The result is that each subsystem can be disabled without taking the platform down. The public product runs without the workspace. The intelligence layer degrades to an empty read when the catalog is absent. The support layer runs with all three siblings inactive. That was designed in, not discovered later.
None of the product is assembled from page builders or off-the-shelf job-board plugins. The data model, the ingestion pipeline, the routing, the SEO and structured data, the matching engine, the resume system, the mail gateway and the operations tooling are purpose-built. WordPress supplies the runtime; everything that makes this a product sits above it.
The university's own page is the authority. The platform collects, normalizes, explains and organizes; it never substitutes. Applying always leaves for the official site.
Nothing is presented as settled without a source. AI work stops rather than deliver a low-quality result, and a failed action never costs the user a credit.
Where a source published nothing, the interface says not provided by source. Guessing to fill a layout is how a data product loses trust.
No probability of acceptance. No grade attached to a person. Recommendations explain themselves in words, and the words can be argued with.
Personalization and analysis activate only on explicit consent. Full export and deletion are first-class. Any message category a user cannot see and switch off is never sent.
Each principle is tied to a shipped behaviour. That is what separates a product rule from a brand statement.
A thin per-source adapter that only extracts and maps, with normalization, validation, de-duplication, lifecycle and visibility centralised. Adding a source became a bounded, repeatable job, and no adapter can invent its own behaviour.
AI never writes to the canonical record. It annotates, validated before display and labelled at the point of use. The factual layer stays auditable back to a source; the explanatory layer can be switched off without touching it.
Separate systems, separate tables, one-way dependency, cross-system reads only through allowlisted contracts that fail closed. Personal data never enters the surface that serves anonymous traffic.
Build the engine, the surface and the evaluation gate, then hold the algorithm behind that gate until it passes, and permanently forbid scores from reaching a member. Ordinal bands and plain-language reasons instead of a fabricated percentage.
Express search as a sentence of chips with per-chip rules, show a live count of what survives, and state plainly which rules exclude versus which only reorder. Users can see the cost of a rule while setting it, and the interface never claims an effect the engine does not have.
Eighteen institutions, eighteen content systems, no shared structure, and no API.
A thin per-source adapter that only extracts and maps, with all shared logic (normalization, validation, de-duplication, lifecycle, visibility) centralised.
Adding a source is a bounded, repeatable job, and no adapter can invent its own behaviour.
Language models are excellent at explanation and unreliable as a source of record.
AI never writes to the canonical record. It annotates. Output is validated before display, labelled at the point of use, provider- and model-agnostic, and activated by an explicit operator decision rather than switched on by default.
The platform's factual layer is auditable back to a source, and the explanatory layer can be improved or switched off without touching it.
Anonymous discovery traffic and private resumes have opposite requirements for caching, access and retention.
Separate systems, separate tables, one-way dependency, cross-system reads only through allowlisted contracts that fail closed.
The public product survives the workspace being disabled, and personal data never enters the surface that serves anonymous traffic.
Ranking people against opportunities produces numbers that look authoritative and are not.
Build the engine, the surface and the evaluation gate, then hold the algorithm behind that gate until it can pass it, and permanently forbid scores from reaching a member.
A personalization layer that is complete in code and honest in production, shipping ordinal bands and plain-language reasons rather than a fabricated percentage.
Preference controls imply more power than the engine actually has, and a user cannot tell which choice removed results.
Express search as a sentence of chips with per-chip rules, show a live count of what survives, and state plainly which rules exclude versus which only reorder.
Users can see the cost of a rule while setting it, and the interface never claims an effect the engine does not have.
Public discovery, offer intelligence, personalized recommendations, the resume studio, the application board, notifications, operations and SEO tooling, designed as one system with one visual language. Grouped here by the appearance each surface ships in, because that grouping is itself the architecture: the public platform runs dark, the private workspace runs dim, and every component resolves in light too.
Every surface above resolves in all three appearances. Only one is shown here, because showing the same screen three times proves the token system once and then becomes a gallery.
Opportunita runs in production. Public discovery, the offer pages and directories, the offer intelligence layer, personalized recommendations, the private workspace, the resume studio and the application board are all live and serving real users. Billing runs live too: checkout, webhook fulfilment, the customer portal and reconciliation, with real payments processed.
Every one of those subsystems ships inert and requires a deliberate activation decision, and the matching engine had to pass its own evaluation gate before it was allowed to serve a member. The project keeps a written service catalogue separating what is shipped from what is partial from what is planned, so "live" is a state the system can prove rather than a claim.
This was not built by a team, and it was not generated by a prompt. It was built the way a small engineering organisation works, with one person holding every role and a deliberate method for keeping quality under that constraint.
I defined the problem and the product, designed the architecture and the ownership boundaries, made the data-model, privacy, AI-behaviour and SEO decisions, directed the interface and the brand, wrote the specifications and acceptance criteria, reviewed and corrected the implementation, ran the QA, and manage releases and deployment.
A large share of the implementation was produced through AI-assisted development, treated as an engineering method rather than a shortcut. The interesting work was not typing code. It was deciding what should exist, what must never happen, and how the parts are allowed to depend on each other.
I defined the problem and the product. I designed the architecture and the ownership boundaries between systems. I made the data-model decisions, the privacy and security boundaries, the AI behaviour rules, and the SEO architecture. I directed the interface, the information architecture and the brand. I wrote the specifications and the acceptance criteria, reviewed and corrected the implementation, ran the QA, debugged production incidents, and manage releases and deployment.
A large share of the implementation was produced through AI-assisted development. I treat that as an engineering method, not a shortcut: I write the requirements and the contracts, define the constraints and the acceptance criteria, review the output, reject what does not meet the contract, and own every architectural and product decision. The interesting work in this project was not typing code. It was deciding what should exist, what must never happen, and how the parts are allowed to depend on each other.
Explicit contracts between subsystems. A written service catalogue that separates shipped from partial from planned. Release gates a subsystem must pass before it goes live. Failure policies decided in advance: fail closed, degrade honestly, never charge for a failed action. Those are the practices that let one person hold a system this size without it quietly becoming untrue.
Decomposing one problem into subsystems with explicit ownership, dependency direction and failure behaviour.
A relational data model, a REST contract, a React application, a background processing pipeline and a document generation system, working as one product.
Dense, data-heavy interfaces that stay legible: faceted discovery, a preference language, an application pipeline, and a document builder.
Ingestion, normalization, validation and enrichment, with AI constrained by evidence, validation, consent and fail-safe behaviour.
Choosing what not to build, what to gate, and which honesty constraints are worth the features they cost.
Versioning, release gates, migrations, deployment, incident debugging and live QA on a running platform.
The thing I would want someone to take from this is not the feature count. It is that a real, messy, fragmented problem was turned into a system with defined boundaries, honest behaviour under uncertainty, and a path a user can actually walk from finding an opportunity to submitting an application.
Opportunita, an academic opportunity platform for PhD, PostDoc and research applicants. Designed, architected and built by Parsa.
You arrived here from a black screen with a name on it. Now you know what one of those projects actually is. Keep scrolling to let it go.