Vinova Lab · Engineering

Engineering guidelines

The conventions every challenge is reviewed against.

Engineering guidelines

Every challenge is reviewed against these conventions. They are published in full so you can read them before you claim anything, rather than discover them in a rejection. Most are enforced automatically when you submit; the rest are what a reviewer looks for.

Every rule has a number. A review that returns work cites the rule it fails — §3.2, not “this could be cleaner”. If a reviewer cannot point at a number, the work is accepted. That constraint exists to protect you, and it is the reason this document is long enough to be specific.

If a brief and these guidelines disagree, the brief wins for that challenge — and tell us, because one of the two is wrong. The exception is §3 and §13, which a brief cannot override.


Part One — How the code is written


1. Reuse before you create

Most rejected work is not wrong. It is a second version of something that already exists, and now there are two things to keep in step.

1.1 Look for it first. Buttons, tables, cards, modals, form fields, empty and loading states already exist as shared components. Use them rather than writing a local variant.

1.2 Pages compose, they do not style. A page holds route composition and business logic. Styling belongs in the shared component or in the stylesheets, not in a class string repeated across three files.

1.3 Missing pattern? Propose it as shared. If the component you need is genuinely absent, build it inside your challenge but build it as if it were already shared: no challenge-specific logic baked in, no assumptions about where it is used. Then say in the pull request that it belongs in the shared layer. We promote it, and the next person gets it for free.

1.4 You are not expected to have access to the shared layer. Most challenges do not grant write access to it, and that is deliberate — a shared component changes for everyone. Proposing is the mechanism, and nothing is lost by proposing something we decide to keep local.

1.5 If you need something shared changed, ask rather than fork it. Open the question on the challenge issue (§10.2). A local copy of something shared is the one outcome this section exists to prevent, and copying because you could not edit the original is still copying.

1.6 The same applies to the backend. Logging, configuration, service-to-service calls and error shapes come from shared modules. A local copy diverges the first time the shared one is fixed.

1.7 A new service starts from the template. Backend microservices are not scaffolded from an empty folder. They start from vinovalab/microservice-template, which already wires up logging, configuration loading, health and status endpoints, and the standard error shape. What you add is the application logic. A service assembled from scratch will be returned even when it works — not because the result is wrong, but because everything in §1.6 arrives with the template, and anything reintroduced by hand is a local copy by another name.

What the template already solves, and how to add your own behaviour to it without rewriting any of it, is set out in Using the service template. Read it before you start: most of what a first delivery gets returned for is described there.


2. Services own their data

Every service has an explicit contract and a boundary. Crossing it is the change that works today and breaks the moment either side moves.

2.1 Ask, do not reach in. Read another service’s data through its API, never by querying its tables. The table is an implementation detail; the API is the promise.

2.2 Ownership is enforced by the owner. The service that owns a record decides who may see it. A caller that filters results itself is a caller that will eventually forget to.

2.3 Contracts are versioned. Changing a response shape is a change to a promise. Add rather than repurpose, and say so.

2.4 Tenancy is part of the boundary. Every query that touches customer data is scoped to a tenant, and the scope comes from the request context (§3.1), never from a parameter. A query that could return another tenant’s row is a defect even when it does not.


3. Security is not your problem — routing around it is

You will not write authentication or authorisation code. It exists, it is tested, and it is not part of any challenge. This section is short because there is almost nothing to do: use what is provided, and never build a path around it.

A brief cannot relax this section.

3.1 Identity arrives already resolved. The authenticated user and their permissions reach your code through the provided context. Read them from there. Never derive identity from a request body, a query parameter, or a header you parsed yourself — those can claim anything, and the platform has already established the truth.

3.2 Use the authorisation helpers, do not reimplement them. Whether a user may perform an operation is answered by the shared authorisation module. If you find yourself writing a role comparison, stop: either the helper exists and you have not found it, or the permission you need does not exist yet — and that is a question for us (§10.2), not something to invent locally.

3.3 Data reaches you through the provided APIs. Never query another service’s tables, never open your own connection to a platform database, never call an internal endpoint that is not documented in your brief. The API is where ownership and tenant scoping are enforced (§2.2); going around it is going around them.

3.4 Never disable a check to make something work locally. If a guard is in your way, it is telling you something about the design. Commenting it out to test and forgetting to restore it is the single most common way a real vulnerability ships.

3.5 Secrets are configuration. Credentials come from the environment. Not from the repository, not from a config file, not from a comment saying it is temporary. A secret in a diff fails the submission automatically (§11.4) and cannot be waived.

3.6 Validate your own inputs anyway. The platform authenticates the caller; it does not know what your endpoint expects. Validate request bodies and parameters against a schema before they reach your logic.

3.7 Nothing sensitive reaches the logs. No tokens, no credentials, no request bodies containing personal data, no full customer records. Log identifiers, not contents.


4. Migrations

You have no access to our databases. Not production, not staging. Every schema change your challenge needs arrives as a migration you write and we apply. Which of the two working modes applies is stated in the brief.

4.1 Working detached. You run your own PostgreSQL locally and create whatever tables your challenge needs. You reach them through the data-service grammar we provide — the same one used across the platform — not through raw SQL scattered through your code. What you deliver is the set of migrations that would produce your schema on a clean database.

4.2 Working in the sandbox. You work against an environment we provision. You still deliver migrations rather than applying them; we run them.

4.3 Migrations must be idempotent. Running one twice must be safe and must leave the same result. In practice: CREATE TABLE IF NOT EXISTS, ADD COLUMN IF NOT EXISTS, indexes and constraints guarded the same way, and any seed insert written so a second run does not duplicate a row. We will run your migration more than once — across environments, and sometimes after a partial failure — and it has to survive that.

4.4 Do not number them. Numbering is global and we assign it on acceptance, because two people delivering in the same week cannot both know what the next number is. Name each file for what it does and for your challenge, and state the order within your own set if the order matters.

4.5 One migration set, scoped to your challenge. Everything your challenge needs and nothing it does not. A migration that touches a table outside your scope is a boundary crossing (§2) wearing different clothes.

4.6 Say what happens if it has to be undone. Provide the reverse where the change allows one. Some changes cannot be reversed, and that is worth knowing before rather than during — say so explicitly instead of leaving it implied.

4.7 The code does not create tables. Nothing creates or alters schema at runtime. If it is not in a migration, it does not exist.

4.8 A destructive change is announced. Dropping a column or a table, or narrowing a type, is called out in the pull request in plain words. It may still be the right change; it is never a quiet one.


5. One shape for errors, one order for configuration

PENDING — 5.2 only. The response shape is not the same across services yet, so 5.2 cannot be cited in a review and cannot block acceptance (§12.3). 5.1 describes what exists today; it will be rewritten again when the shared error module lands.

5.1 An error carries its status, and the framework does the rest. Throw a plain Error with a numeric status property on it. The global handler installed by serverFactory reads that property, answers with that HTTP status — 500 when there is none — and logs the message.

throw Object.assign(new Error("endDate precedes startDate"), { status: 400 });

Two things follow, and the second one surprises everybody once:

  • Do not invent an error class, and do not invent a response shape. There is no shared error module yet: several services keep a two-line local httpError(status, message), and that is the most you should add.
  • The message you throw does not reach the caller. The global handler logs it and answers { ok: false, error: "Internal server error" }, deliberately — an unhandled error must not leak internals. When the caller has to read a specific message, the route answers for itself:
if (!folder) return res.status(404).json({ error: "Folder not found." });

That is the shape the overwhelming majority of endpoints already use, and until 5.2 says otherwise it is the one to copy: { error: "<message>" }, with the status on the response and nothing else in the body.

5.2 Errors look the same everywhere. The same response shape from every service, so the frontend handles failure once instead of per endpoint. [Exact field list, types and status-code mapping to be filled in. Today roughly one in ten endpoints answers { ok: false, error } and the rest answer { error }; the two have to converge before this rule can be applied.]

5.3 Fail loudly, not quietly. A missing configuration should stop the service, not make it fall back to a default that is wrong in a way nobody notices for weeks.

5.4 Configuration has a priority. Environment, then settings, then default — in that order, always. A value that appears in two places resolves the same way every time.

5.5 An error message says what to do. The shape is for the machine; the message is for a person reading a log at an inconvenient hour. “Validation failed” is not a message. “Validation failed: endDate precedes startDate” is.

5.6 Say what you assumed. When a brief leaves something open, write down the assumption in the pull request. It is faster to correct an assumption than to guess what you meant. When the gap stops you from starting, ask instead — see §10.


6. A change brings its own tests

6.1 New behaviour comes with tests. A challenge that adds an endpoint, a rule or a branch adds the tests that cover it. This is part of the deliverable, not a bonus, and a submission without them is incomplete rather than untidy.

6.2 A fix comes with the test that would have caught it. Reproduce the bug in a failing test first, then make it pass. It is the only way anyone can tell later that the bug is actually gone.

6.3 Test behaviour, not implementation. A test that breaks when you rename a private method is a test the next person will delete. Test what the thing promises.

6.4 Tests do not reach the network or a real database. They run offline, in any order, with no shared state between them. A test that passes only when run second is a failing test that has not noticed yet.

6.5 Fixtures are synthetic. Test data is generated, or drawn from the synthetic fixtures provided with the brief. Never from production, never from a customer export — see §13.4.


7. A new dependency is a decision

The most common way an outside change introduces risk, and the easiest one to miss in a diff.

7.1 Prefer what is already there. The shared modules and the standard library first. A dependency added for one helper function is a dependency we maintain afterwards.

7.2 Say why, in the pull request. One sentence on what it does and what you would have had to write instead.

7.3 The licence has to be permissive. MIT, Apache-2.0, BSD, ISC. Anything copyleft — GPL, AGPL, LGPL — needs asking first, and the answer is usually no.

7.4 No known vulnerabilities. Checked automatically on submission (§11.4). Adding a package with an open advisory fails the check.

7.5 Pin it. Exact versions, and the lockfile is committed. A build that resolves differently tomorrow is not a build.


8. How the work reads

The parts a machine cannot check but the next person notices immediately.

8.1 English throughout. Code, comments, commit messages, pull requests and documentation. Our collaborators are spread across a dozen time zones and it is the only language all of us share.

8.2 A commit message says what changed and why. Not fix, not update, not commit. If the change corrects an earlier attempt, say what the earlier attempt got wrong. Six months from now the log is the only account of how the code got here.

8.3 One pull request, one challenge. Do not fold in an unrelated tidy-up, however small. If you find something worth fixing outside the brief, open an issue.

8.4 The pull request explains itself. What it does, what you assumed (§5.6), anything a reviewer should look at first, and how to run it if that is not obvious. A reviewer should not have to reconstruct your reasoning from the diff.

8.5 A module explains how to run it. If the challenge produces something standalone, it comes with a README covering purpose, configuration, how to start it, and how to run its tests. Documentation is part of the deliverable.

8.6 Documentation that no longer matches the code is a defect. If your change makes an existing README or comment wrong, fix it in the same pull request.


Part Two — How the work runs


9. One challenge at a time

A challenge is allocated to one person, and you hold one allocation. It is what makes the board honest: what is listed as open really is open, and nobody is waiting on work that someone claimed and set aside.

9.1 Claiming takes it off the board. From the moment you claim it, the challenge is yours and nobody else can start it. An idle claim is work that exists for no one.

9.2 Finish it or release it. To take another, the one you hold has to be delivered and accepted, or released. Releasing costs nothing and needs no reason: it returns to the board and someone else can pick it up.

9.3 Every challenge has a window. The delivery window is published with the brief, before you claim it. You know how long you have while you are still deciding whether to take it.

9.4 The window expires on its own. When it runs out the challenge returns to the board automatically and your allocation ends. There is no penalty and nothing to explain.

9.5 The window pauses while the work is with us. From the moment you submit until a reviewer responds, the clock stops. Review time is never taken out of your time.

9.6 Repeatedly claiming and letting windows expire is not a penalty, but we do notice. It costs you nothing directly. It is simply part of what we know when allocating the next thing.

The window sets a deadline for a result. It says nothing about when you work, how many hours you put in, or whether you take the whole period or an afternoon.


10. When the brief does not answer

10.1 Assumptions go in the pull request; blockers go before you start. If a detail is open but you can proceed sensibly, write down what you assumed (§5.6). If the contract is ambiguous in a way that changes what you build, ask.

10.2 Ask on the challenge issue, in writing. So the answer is attached to the brief, and the next person to read it sees the same thing you did.

10.3 We answer within one working day, and the window pauses while you wait. You should never have to choose between guessing and losing a day.

10.4 A brief that turns out to be wrong is our problem, not yours. If the acceptance criteria describe something that cannot be built as written, say so. We fix the brief. Nobody is expected to deliver an impossible specification and nobody is penalised for finding one.


11. What runs when you submit

The same checks for everyone, run as many times as you like. The report is a list of what did not pass, not a verdict.

11.1 It builds. Types check, the project compiles, nothing depends on something that is not declared.

11.2 Tests pass. Yours and everyone else’s. A change that breaks an existing test is telling you something.

11.3 The brief is satisfied. The acceptance criteria published with the challenge, point by point. They were written before you started, and they do not move.

11.4 Nothing dangerous is in the diff. No secrets, no credentials, no dependency with a known advisory. This check cannot be waived by a reviewer or by a brief.

A reviewer reads the diff afterwards. That is where reuse, boundaries and naming are judged — the parts a machine cannot check but another developer notices immediately.


12. Review, acceptance and payment

12.1 Delivered is not accepted. Work is delivered when the automated checks pass and the pull request is open. It is accepted when a reviewer approves it, and acceptance is what triggers payment.

12.2 A review either accepts or returns a list. There is no third outcome. The list is specific, and every item on it cites either an acceptance criterion from the brief or a numbered rule in this document.

12.3 A reviewer cannot reject on preference. “I would have done it differently” is not a finding. If a reviewer cannot cite a number, that point does not block acceptance — and you are entitled to say so in the pull request.

12.4 Two rounds, then a conversation. You get two rounds to address a review. If a third is needed, something was unclear rather than wrong, and we talk instead of iterating. Starting that conversation is our job, not yours.

12.5 If you think a review is wrong, say so in the pull request. It gets read by someone who was not the reviewer. Disagreeing with a review has never cost anyone an allocation.

12.6 A challenge is accepted or it is not. There is no partial acceptance and no partial payment. If a brief turns out to be substantially larger than it looked, tell us before the window ends — we will split it or extend it, and either way you are paid for what you built.

12.7 Payment follows acceptance within ten working days, in the currency and by the method agreed when you joined.


13. Confidentiality, customer data and AI

A brief cannot relax this section.

13.1 What you see stays here. The codebase, the briefs, and anything you learn from them are confidential.

13.2 The work belongs to Vinova Lab. You are welcome to say publicly that you build modules for us. You may not publish the code, screenshots of it, or the contents of a brief.

13.3 Use AI as you like — on code. Agents are part of the workspace. Use whatever assistant, editor or model you prefer on source code, on briefs, and on the synthetic fixtures we provide. What is reviewed is the result, and you are responsible for it either way.

13.4 Customer data never goes into a personal AI tool. Not as an input, not as an example inside a prompt, not paraphrased. Customer data means: anything under /fixtures/real, anything a brief marks confidential, the contents of any production or staging database, exports and logs drawn from them, and screenshots containing any of the above.

13.5 When a challenge involves customer data, use the Vinova instance. We operate a private, self-hosted model for exactly this. Access is issued with the challenge that needs it. It is not a lesser tool and there is no approval to request — it is simply the one you use when real data is in play.

13.6 Most challenges never touch customer data. Synthetic fixtures are provided for every brief that needs data, and they are built to be representative. If you find yourself wanting real data to make progress, that is a question for us (§10.2), not something to solve locally.

13.7 If you are unsure whether something counts, it counts. Ask. Nobody has ever been thought less of for asking this question, and the alternative is not recoverable.

13.8 Responsibility does not transfer. Code an agent wrote is code you submitted. Review it as you would review a colleague’s.


14. What we do not ask

These conventions describe the result, not the method. Everything else remains yours.

14.1 No hours, no presence. Within the delivery window, when and how you work is your business. Nobody counts the days you spent on it.

14.2 No meetings and no status updates. The brief is the specification and the gate is the review. Nothing is scheduled around you.

14.3 No obligation to take anything. You decline without a reason and without consequence. Nothing is assigned to you.

14.4 No exclusivity. What you do outside our board is not our business.


Versions

These guidelines are versioned. The version in force when you claim a challenge is the version that challenge is reviewed against — a change published mid-window never applies to work already claimed.

Each version is published at vinovalab.ai/work-with-us/engineering-guidelines with its date and hash. Substantive changes are announced before they take effect.

Version 1.0.