# european-tech-internships-2026: a Markdown job list and the EarlyCareers platform behind it

> The repository holds a curated table of European internships and new graduate roles and, in the same tree, the EarlyCareers application that stores a larger opportunity database with search. The list is generated output you can read in seconds; the platform is a self hosted stack you have to run.

**LorenzoLaCorte/european-tech-internships-2026** — A regularly updated collection of European tech internships, new graduate roles, and research opportunities. It also contains the code for EarlyCareersEU, an open-source web platform containing a larger and more detailed database of opportunities, and an advanced search.

- Repository: https://github.com/LorenzoLaCorte/european-tech-internships-2026
- Website: https://earlycareers.fabifont.dev/
- Stars: 918 · Forks: 16
- Language: TypeScript
- License: not declared
- Published: 2026-10-20 · Updated: 2026-10-20 · Language: en
- Canonical page: https://hysenlabs.com/projects/lorenzolacorte-european-tech-internships-2026

## Two products sharing one repository tree

This repository is two things at once, and the top level layout makes that visible. The first is a curated list of European tech internships, new graduate roles, and research opportunities, published as Markdown tables inside README.md. The second is the source code of EarlyCareers, a web platform at earlycareers.fabifont.dev that the project description describes as holding a larger and more detailed database of opportunities with an advanced search on top.

The directories confirm the split. Alongside README.md and README.template.md sit backend/, frontend/, docker-compose.dev.yaml, docker-compose.prod.yaml, justfile, a scraped/ directory, a fixed/ directory, and a .pre-commit-config.yaml. So the README is generated output, not the place you edit, which is why a template file sits next to it and why two data directories exist.

For a job seeker the interesting half is the table, which is readable in ten seconds. For an engineer the interesting half is the application, and that half carries a real cost: a database, a backend, a frontend and an environment file. Keeping both in one tree is convenient, and it is also the main thing to understand before you touch anything.

## Template markers show exactly where the scraper writes

The README is assembled from a template, and it says so through comment anchors. Each table sits between a pair of markers, for example the internship section runs from a BEGIN INTERNSHIPS comment to an END INTERNSHIPS comment, with the same pattern for the machine learning internships, the new graduate roles and the PhD positions. Anything a contributor adds outside those boundaries survives; anything inside them gets replaced on the next run of the scraper.

The scrape itself is not in this repository. The listing file links a separate project under a Scraper heading, so the pipeline is split across two code bases, and this one is expected to be the destination of that tool.

The detail worth knowing is that the tables are not equally filled. The internship and machine learning internship sections contain only a header row with no entries between the markers, while the new graduate section holds several dozen rows and the PhD section holds a smaller set. Nothing in the documentation explains that difference, so treat the empty sections as an unrun or filtered step rather than as a permanent state of the project.

## Four columns, one unnamed cell, and a link that carries the value

Every row has the same shape: a company, a title, a location and a link, and the link points at a LinkedIn job posting. The data rows carry one more empty cell than the header row names, which suggests the template has a fifth column that the header line never labels. A reader cannot tell what that field was meant to hold, and it is worth checking the template before writing a parser against the shape.

Locations are raw strings, not structured fields. You get values such as a city with region and country, a city with country, and in at least one case just a country. There is no separate country column, so any filtering has to rely on string matching against an unnormalised value, which is exactly the kind of thing a search backend is for.

What the table does not carry is as telling as what it does. No salary range, no remote flag, no closing date, no team or seniority apart from what is baked into the title text. For a human scanning forty rows that is workable. For a query interface over thousands of rows it is not, which is the gap the platform is trying to close.

## Ports 6000, 6001 and 6002 define the local estate

The environment sample is short and tells you the whole local topology:

```bash
MODE=dev

LOCAL_POSTGRES_PORT=6000
LOCAL_BACKEND_PORT=6001
LOCAL_FRONTEND_PORT=6002
```

Three services, three consecutive ports, and a mode flag. The remaining variables point the rest of the stack at them:

```bash
DB_URI=postgresql+psycopg://owner:password@host/db?sslmode=require

VITE_API_BASE_URL=http://localhost:6001
```

The database URI uses the psycopg driver with SSL required, the allowed CORS origins are built from the frontend port for both http and https, and the Vite variable points the frontend at the backend on port 6001. Note the shape of the sample credentials: owner, password, host and db are placeholders, so you must supply a real database URL before the backend will connect. The `set dotenv-load` line in the task file means these values are loaded from the environment file automatically for the commands that follow.

## just targets cover Docker, dependencies and the FastAPI dev server

The justfile is the front door for local work, and it splits into two halves. The Docker half picks a compose file from the MODE variable and a flag with it, so the same recipe runs detached in production mode and in watch mode in development:

```bash
docker-up:
    docker compose -f {{ target }} up {{ docker_flags }} --build

backend-deps:
    cd backend && uv sync
```

The Python half is separate. A virtual environment path is chosen per operating system, dependencies are installed with uv sync inside the backend directory, a pre-commit target runs the hooks across all files, and the dev target starts the server from a specific entry point:

```bash
backend-dev: backend-deps
    {{ backend_venv }}/fastapi dev backend/earlycareers/main.py
```

Two things stand out for an engineer reading this. The backend is a Python application served by FastAPI, with uv managing dependencies, even though the repository's primary language is TypeScript; the frontend is the side that carries the type system. And the backend entry point is a file path under backend/earlycareers, so the application package name is earlycareers and the module is main.py.

## No licence, no releases, and data that stops being true

The licence field on the repository is not set, and the repository has no GitHub releases. That is worth pausing on before you copy any of this code into your own work, because a missing licence means nobody has stated the terms under which the code is reused. It is a fact about the repository rather than a legal opinion, and it is the kind of gap that blocks a company from shipping the code unchanged.

For freshness, the last push was on 2026-10-19, so the data is refreshed regularly. There is no tagged version to compare against that date, though, so you cannot tell from the repository alone whether a given row is one hour old or one month old, and you cannot pin a deployment to a known state.

The dataset itself has a built in expiry. Every row is a pointer to a LinkedIn posting, and postings close, get filled, or get rewritten. The documentation does not explain how a closed posting is removed, whether a dead link is detected, or how often the whole list is regenerated. If you build on top of it, treat the link column as a cache that goes stale rather than as a source of truth.

## When the Markdown table is enough and when you need the platform

There are two sensible ways to use this repository and they have very different costs.

The low cost path is the Markdown list. Clone the repository, read the tables, and subscribe to the commits to see new roles. There is nothing to install, no database to provision and no port to remember. If you want a job, this is the whole product and the platform is irrelevant to you.

The higher cost path is EarlyCareers, and the difference in approach is that a database turns free text into queryable records. Once opportunities live in Postgres behind an API, a frontend can filter by location, split internship from graduate and PhD entries, and add a search box, none of which the four column table can support. The price is that you now own the deployment: Docker Compose in dev or prod mode, three services, a real database URL, migrations of your own, and CORS origins configured for your hostname.

A smaller middle option exists for people who want the data without the UI, and it is the design of the repository itself: the Markdown is generated from structured files, so a script could read the tables or the intermediate directories and load them into your own tools. That depends on the internal format of the scraped and fixed directories, which the documentation does not describe, so you would be reading the code rather than a spec.

## Conclusion

Use the Markdown tables if you only need to browse roles, and run EarlyCareers yourself if you need filtering over a larger set with search on top. Do not treat this as a released product: the repository declares no licence, publishes no releases, and gives you a last push date rather than a version to pin. Before adopting it, check whether the internship table between the BEGIN INTERNSHIPS markers is populated, because the README ships that section empty, confirm the licence yourself, and verify that a handful of the LinkedIn links still resolve, since each row is only a pointer to a posting that can close.

## FAQ

### How do I run the EarlyCareers platform locally?

The justfile defines a docker-up recipe that starts the compose file chosen by the MODE variable, in watch mode for dev and detached for prod, and a backend-dev recipe that installs dependencies with uv sync and then serves backend/earlycareers/main.py with fastapi dev. You also need to fill in DB_URI, since the sample in .env.sample is a placeholder.

### What licence does european-tech-internships-2026 use?

The licence field on the repository is not set, and no licence file is listed among the top level entries, so the repository states no reuse terms. Check with the maintainers before shipping any of the code.

### Where do the internship listings come from and how often are they updated?

The listing file points to a separate scraping library, and the repository keeps scraped and fixed directories plus a template file that holds the table markers. The last push was on 2026-10-19, but the documentation does not give a refresh schedule or a rule for removing closed postings.

### Which technologies does EarlyCareers use?

TypeScript is the primary language and the frontend reads VITE_API_BASE_URL, while the backend is a Python service started with fastapi dev from backend/earlycareers/main.py, backed by Postgres through DB_URI with the psycopg driver and managed with uv.

## Sources

- [Issues](https://github.com/LorenzoLaCorte/european-tech-internships-2026/issues)
- [LorenzoLaCorte/european-tech-internships-2026 on GitHub](https://github.com/LorenzoLaCorte/european-tech-internships-2026)
- [Project website](https://earlycareers.fabifont.dev/)
- [README](https://github.com/LorenzoLaCorte/european-tech-internships-2026/blob/main/README.md)

---

Hysen Labs editorial analysis, written from the project's own repository and release notes. Cite the canonical page: https://hysenlabs.com/projects/lorenzolacorte-european-tech-internships-2026
