Tymeslot

tymeslot.app
Tymeslot

Open-source (AGPL-3.0), self-hostable scheduling and calendar platform. Guests book without creating an account; hosts get a full calendar with two-way sync to Google, Outlook and iCloud. A managed cloud plan is offered too, not self-hosted.

Open Source

Tymeslot Source Code

Author

Tymeslot

Description

Open-source meeting scheduling platform built with Elixir/Phoenix LiveView. Self-host or use the managed cloud at tymeslot.app.

#appointment#appointment-booking#appointment-scheduling#appointments#calendar#calendars#calendly-alternative#docker#elixir#phoenix-liveview#postgres#scheduling

Homepage

https://tymeslot.app

Repository

  • LicenseAGPL-3.0
  • Created07 Aug 25
  • Primary languageElixir
  • Size182,366 KB
  • Stars173
  • Forks24
  • Watchers173

Language Usage

Language Usage

Project Health

  • Last commit2 hours ago
  • Open issues2
  • Latest releasev1.10.1

Top Contributors

Recent Commits

  • Luka Breitig(17 Aug 26)

    chore: bump version to 1.10.1 Signed-off-by: Luka Breitig <[email protected]>

  • Luka Breitig(17 Aug 26)

    chore: update dependencies and migrate to req 0.7 Clears three security advisories: phoenix_live_view 1.2.9 (EEF-CVE-2026-64941), postgrex 0.22.4 (EEF-CVE-2026-66838) and html_sanitize_ex 1.5.4 (EEF-CVE-2026-68749). Only the html_sanitize_ex one was reachable, on the user-input paths through UniversalSanitizer. req 0.7 moves the Finch adapter's settings under a keyword list, so HTTPClient now passes `finch: [name: Tymeslot.Finch]`; the bare atom form is deprecated and goes away in 0.8. This lifts the 0.6.x pin that was waiting on exactly that migration. None of the other breaking changes in 0.7 apply: no `:params` usage, no removed steps, and GET requests never carry a body because an empty one is not attached. That Finch branch is unreachable from the suite, since config/test.exs routes every test through the `:plug` branch, so it gains a test that clears the plug and asserts Req reports no deprecated adapter option. Also updates castore, ex_machina, hackney, oban, phoenix, sobelow and tidewave. Signed-off-by: Luka Breitig <[email protected]>

  • Luka Breitig(17 Aug 26)

    fix(core): reject login redirect targets containing tab or newline characters Browsers strip ASCII tab, LF and CR from a URL before parsing it, so a target such as `/<tab>/evil.com` satisfied the relative-path check and was then fetched as `//evil.com`. Phoenix refuses those characters when it writes the Location header, so such a request raised rather than redirecting. Reject them in the raw string and in both URL-decoded forms, so an encoded `%09` payload is caught too. The property test already generated these characters but its invariant only inspected the literal result, which is why 500 runs a build never caught this. It now checks the tab/LF/CR-stripped string a browser actually resolves. Signed-off-by: Luka Breitig <[email protected]>

  • Luka Breitig(17 Aug 26)

    chore: bump version to 1.10.0 Signed-off-by: Luka Breitig <[email protected]>

  • Luka Breitig(17 Aug 26)

    test(core): cover the schedule rekey against seeded availability data The migration that moves weekly availability and date overrides onto availability schedules had no test that could fail. Both the dirty seed and the ordinary suite met those tables empty, so its UPDATEs matched nothing, its orphan sweep deleted nothing, and it passed having proved only that the DDL parses. The seed now carries a full week, breaks and all three override types across three profiles, one of which holds NULL policy values so the backfill's COALESCE is executed rather than merely present. The test takes a profile-keyed census before migrating and compares it against a schedule-keyed one after, so re-pointing every row at whichever schedule came first no longer passes. Counts are asserted non-empty first, since two empty results compare equal and that is the weakness being fixed. Also adds the missing half of the per-type guarantee: a date offered for a meeting type on a long booking window is refused when submitted against one on a short window. Availability windows themselves are still not re-checked at booking time, which is unchanged and out of scope here, so the advance window is the dimension where the two halves can be shown to agree per meeting type. Signed-off-by: Luka Breitig <[email protected]>

  • Luka Breitig(17 Aug 26)

    fix(core): say the default schedule still catches everything unassigned The availability page tells you which meeting types a schedule applies to. For the default schedule it had two answers and picked the wrong one whenever a meeting type named it explicitly: it listed those types and dropped the fact that the schedule also applies to every type that names nothing at all. That understates what editing those hours affects, on the one schedule where the consequence is widest. Changelog: skip Signed-off-by: Luka Breitig <[email protected]>

  • Luka Breitig(17 Aug 26)

    fix(core): keep the availability audit working without a default schedule The audit read the profile's default schedule and dereferenced its id straight away, so a profile without one crashed the run rather than reporting on it. Every profile gains a default schedule on creation and cannot delete it, so this only happens on a database that has lost one. That is precisely the state the audit exists to diagnose, and it is the one situation where it has to keep working: the tool is reached for when availability is already behaving oddly. A nil schedule id is what the engine treats as "no schedule resolvable", so the audit now measures the fallback hours a visitor would actually be offered. The weekly pattern and overrides are left out of the config rather than prefetched empty, which would have claimed a closed week instead of letting those fallback hours apply. Changelog: skip Signed-off-by: Luka Breitig <[email protected]>

  • Luka Breitig(17 Aug 26)

    refactor(core): resolve the scheduling policy fallback from one table Buffer, minimum notice and the advance booking window had four separate answers to "what applies when no schedule resolves": the schema's column defaults, a pair of identical private tables in the two scheduling helpers, per-call defaults in Bookings.Policy, and bare literals in the engine. The availability page held a fifth that disagreed outright, falling back to a zero buffer and a 24-hour notice. Nothing user-visible depended on the disagreement, since every path normally has a schedule. It is a correctness hazard rather than a bug: the offered slots and the booking-time re-check reach the policy through different code, so any drift between those tables shows up as a slot that is offered and then refused. Constraints.scheduling_policy_defaults/0 is now the one table. Schedules.policy/2 reads it for a resolved-or-nil schedule and Calculate.config_policy/1 for a config map, which also removes the duplicated private helpers and eight magic numbers. The schema keeps literal field defaults so it carries no compile-time dependency on Constraints, which would add compile-connected edges to a graph already close to its ceiling; a test pins the two together instead. The onboarding preference steps now take plain integers rather than the schedule struct, so the components no longer disagree with their parent about whether a nil schedule is reachable. Signed-off-by: Luka Breitig <[email protected]>

  • Luka Breitig(17 Aug 26)

    fix(core): stop retrying webhook deliveries the endpoint will always reject Every non-2xx response was treated as retryable, so a 404 from a subscriber's endpoint consumed all five attempts in under twenty seconds, then raised Oban.PerformError and produced an admin alert about what is a subscriber's own misconfiguration. Route the status through ErrorAnalysis.classify_error/1, already the shared answer to whether an HTTP failure is worth another attempt: 5xx, 408, 425 and 429 keep retrying, while 401, 403 and 404 discard. An intentional discard emits job:stop rather than job:exception, so it no longer reaches the failure alerter. The failure is still recorded, unconditionally on the discard path since no later attempt will run, so a permanently broken endpoint still advances towards being auto-disabled. The sibling delivery workers already did this: SlackWorker discards a 404 from a revoked incoming webhook, TelegramWorker discards a 401. WebhookWorker was the one that never got it, so reuse the existing classifier rather than adding a fourth copy of the status list. Also pass {:discard, _} through handle_delivery_error/4, which otherwise matched only {:error, _} and would have raised FunctionClauseError on the new terminal verdict. Signed-off-by: Luka Breitig <[email protected]>

  • Luka Breitig(17 Aug 26)

    fix(core): keep signup usable when the verification email is rate limited Verification.verify_user_email/3 returns the three-element {:error, :rate_limited, message} that VerificationBehaviour declares alongside the two-element form, but Registration matched only the latter, so a refusal raised CaseClauseError out of the signup LiveView. The crash was the visible half. It struck after the user row was committed but before the profile and the registration broadcast, leaving an account that could not be reached: signing up again was rejected as a duplicate, signing in was rejected as unverified, and no verification email had been sent because the token is only persisted past the rate-limit check. Handle the refusal, and create the profile and broadcast the registration before attempting the email. Those complete the account; the email only announces it. A send that fails or is refused now leaves a whole account and an error the user can act on, and the resend flow can finish the job. The verification module is resolved at runtime via Application.get_env/3 so it can be mocked, which is why Dialyzer could not see the missing clause. The new tests exhaust the real limit rather than mocking it, and cover the account being left complete as well as the crash itself. Signed-off-by: Luka Breitig <[email protected]>

  • Luka Breitig(17 Aug 26)

    fix(core): stop one visitor's rate limit from blocking everyone behind a proxy Behind a reverse proxy on a dual-stack listener, the peer arrives as an IPv4-mapped address such as ::ffff:172.18.0.1. That is an 8-element tuple, so it matched none of the IPv4 clauses in the trusted-peer check, the forwarded headers were discarded as untrusted, and every LiveView visitor resolved to the proxy's own address. Every IP-keyed rate limit then became a single allowance shared by the whole deployment. Ordinary traffic was enough to exhaust it: five verification emails an hour across all users, after which the next signup was refused. Normalise the peer before matching, and report it in plain IPv4 form so the socket path and the conn path key the same visitor to the same bucket. The mapped decode moves into PrivateIPv6.unmap/1 and is shared with private?/1 rather than written twice. The trusted-peer ranges are deliberately left narrower than PrivateIPv4.private?/1, which also covers 0.0.0.0/8, 100.64/10 and 169.254/16. That predicate asks whether an address is unroutable, for SSRF; this one asks whether a peer may speak for someone else. Trusting a CGNAT or link-local peer would let a client forge x-forwarded-for and evade the very limits this restores. Every existing test for the trusted-peer path used a 4-element tuple, the one shape production never produces, which is how this survived. The new coverage asserts the consequence rather than the mechanism: two visitors behind one proxy must not share a bucket, so it still fails if the collapse returns by another route. Signed-off-by: Luka Breitig <[email protected]>

  • Luka Breitig(17 Aug 26)

    chore(core): bump Cloudron base image to 5.1.0 Signed-off-by: Luka Breitig <[email protected]>

  • Luka Breitig(14 Aug 26)

    Merge branch 'availability-schedules'

  • Luka Breitig(14 Aug 26)

    feat(core): announce availability schedules to existing users Adds the catalogue entry and illustration for named availability schedules, so users who signed up before the feature shipped see it in the dashboard announcement carousel. Copy is translated into all five locales, as the catalogue requires. The illustration shows the schedule tabs alongside meeting types carrying matching accent colours, mirroring how the availability screen links the two. The CTA points at the feature's documentation article. The feature itself already carries the changelog line, so this companion entry stays off it rather than producing a second bullet for the same capability. Changelog: skip Signed-off-by: Luka Breitig <[email protected]>

  • Luka Breitig(14 Aug 26)

    fix(core): say when a duplicated schedule leaves its date overrides behind Duplicating copies the hours, breaks and booking rules but deliberately not the date overrides, which name specific calendar days. The confirmation said only "Schedule duplicated", so a host who had blocked a holiday found it quietly missing from the copy. The message now names the omission, and only when the source actually had overrides to lose. Changelog: skip Signed-off-by: Luka Breitig <[email protected]>

  • Luka Breitig(14 Aug 26)

    fix(core): keep a reschedule on the meeting's own type The reschedule link carries only the meeting uid, and the meeting type was re-derived from the duration in the URL. That lookup matches slugs, so "30min" matched nothing and the page fell back to the profile's default schedule: slots were offered from the default while the submit was validated against the original type's schedule, which is the last way left to be shown a slot that cannot be booked. The type now comes from the meeting being moved; duration matching stays for visitors who are still choosing what to book. Changelog: skip Signed-off-by: Luka Breitig <[email protected]>

  • Luka Breitig(14 Aug 26)

    docs(core): describe named availability schedules in the README The availability section still described hours you "set once", which was the one-schedule-per-profile model, and its screenshot showed the retired editor. Signed-off-by: Luka Breitig <[email protected]>

  • Luka Breitig(14 Aug 26)

    test(core): move the reschedule notification fixture onto a schedule The setup still handed buffer_minutes to the profile factory after the booking policy moved onto availability schedules, so the whole module failed to set up. Signed-off-by: Luka Breitig <[email protected]>

  • Luka Breitig(14 Aug 26)

    fix(core): offer the meeting type's own schedule on the booking page The calendar grid and week strip resolved the profile's default schedule regardless of which meeting type the visitor had chosen, so a type on any other schedule rendered the default's open days on first paint and whenever the availability fetch failed. A weekend-only type showed Mon-Fri as bookable until the fetch landed, then offered no slots. The advertised booking window came from the default too, and it disables the calendar's forward navigation, so a meeting type whose schedule allowed a longer window had its later dates walled off at the default's limit. Both now resolve from the meeting type, falling back to the default only before one is chosen. Changelog: skip Signed-off-by: Luka Breitig <[email protected]>

  • Luka Breitig(14 Aug 26)

    fix(core): reject a meeting type pointed at another host's schedule The schedule id posted with a meeting type was only checked by a foreign key, which proves the row exists rather than who owns it, and the picker's options are a client-side constraint a forged event ignores. Because the id is looked up unscoped when the booking page resolves its hours, an unowned one would have driven that meeting type's public availability from someone else's week. The write now resolves the id against the host's own profile, using the scoped query that already existed for this and had no callers. Changelog: skip Signed-off-by: Luka Breitig <[email protected]>

  • Luka Breitig(14 Aug 26)

    fix(core): clear cached availability when a schedule changes Availability is cached per user for two minutes, and only weekday, break and override edits were invalidating it. Editing a schedule's buffer, notice or booking window, promoting a different schedule to default, duplicating one or deleting one all change what the slot engine computes, so the booking page could keep offering the old hours until the entry expired. Promoting and deleting are the sharp cases: both re-point every meeting type that follows the default, not just the schedule being edited. Changelog: skip Signed-off-by: Luka Breitig <[email protected]>

  • Luka Breitig(14 Aug 26)

    feat(core): redesign availability around schedule tabs A profile's schedules now read as a tab strip rather than a dropdown, with tabs and content sharing one panel so the hours below always belong to a visibly selected schedule. Each schedule owns a colour, worn by its tab, the panel frame and the chip that picks it on a meeting type, so the same schedule looks the same on both screens. The weekday editor collapses to one row per day: toggle, name, hours and breaks inline, with copy-to-days and clear-day moved into a per-row menu. Seven days no longer cost seven cards. The grid view goes with it, and break pickers only offer times inside the day's own hours instead of rejecting the choice afterwards. Schedules are capped at five, which is what keeps the strip legible; the add button stays in place and greys out at the cap rather than disappearing. Changelog: skip Signed-off-by: Luka Breitig <[email protected]>

  • Luka Breitig(14 Aug 26)

    test(core): render the git-cliff configs in the suite The changelog configs are release tooling with no compile-time surface, so a broken template showed up only once a release was already being cut. Build a throwaway repository with one commit per rendering decision and render it through both real configs, which covers the commit parsers and the body template together rather than either in isolation. The tests need the git-cliff binary, so they are excluded wherever it is absent and the verify workflow now installs it, at the version the release workflow already pins. Signed-off-by: Luka Breitig <[email protected]>

  • Luka Breitig(14 Aug 26)

    chore(core): drop the redundant core scope from generated changelogs Both git-cliff configs prefixed every entry with its commit scope, and that scope is `core` on all but a handful of lines, so the prefix repeated itself down the whole list without distinguishing anything. Suppress the scope when it is `core` and keep narrower ones, which do still say something. CHANGELOG is regenerated to match; the result is otherwise identical to a fresh git-cliff run over the same history. Signed-off-by: Luka Breitig <[email protected]>

  • Luka Breitig(14 Aug 26)

    Merge branch 'main' into availability-schedules # Conflicts: # lib/tymeslot/availability/availability_actions.ex # priv/gettext/cs/LC_MESSAGES/dashboard_availability.po # priv/gettext/dashboard_availability.pot # priv/gettext/de/LC_MESSAGES/dashboard_availability.po # priv/gettext/en/LC_MESSAGES/dashboard_availability.po # priv/gettext/fr/LC_MESSAGES/dashboard_availability.po # priv/gettext/it/LC_MESSAGES/dashboard_availability.po # priv/gettext/uk/LC_MESSAGES/dashboard_availability.po

  • Luka Breitig(14 Aug 26)

    chore: bump version to 1.9.0 Signed-off-by: Luka Breitig <[email protected]>

  • Luka Breitig(13 Aug 26)

    test(core): mock update_credentials in the video re-authorization tests The Teams and Google Meet re-authorization tests mocked VideoIntegrationQueries.update/2, but the callback path now runs through Video.match_or_create_oauth_integration/6, whose reauthorize_existing/4 calls update_credentials/2 instead. The queries module is mecked with :passthrough, so the unmatched expectation fell through to the real implementation, which tried to update a struct the test had only built. Both tests failed with Ecto.StaleEntryError. Signed-off-by: Luka Breitig <[email protected]>

  • Luka Breitig(13 Aug 26)

    chore(core): add an incremental-PLT dialyzer task `mix dialyzer.incremental` runs OTP's incremental analysis against its own .iplt file, alongside the existing `mix dialyzer`, which is unchanged and remains the gate. A classic PLT is cached wholesale: dialyxir re-checks it by hashing mix.lock plus the application list, and when that hash moves it re-verifies every module rather than the ones that changed. That trigger is also blind to a path dependency's source changes, which move neither input. An incremental PLT tracks per-module hashes itself; measured against a one-module edit, --metrics-file reported one module of 4475 re-analysed. Measured here: 536s cold and 40s for a one-module edit, against 589s and 133s for the classic task. Both modes report identical accounting (86 errors, 86 skipped, 0 unnecessary skips), so .dialyzer_ignore.exs needs no changes. Dialyxir cannot reach incremental mode through its own task, which builds a fixed argument list, but its runner passes unrecognised options straight to :dialyzer.run/1 and returns the same warning list either way. Calling Dialyxir.Dialyzer.dialyze/1 therefore keeps its Elixir-shaped formatting, ignore-file filtering and unused-filter reporting. :dialyxir joins plt_add_apps for the same reason :credo is there: it is runtime: false, so plt_add_deps: :app_tree omits it, and this task calls into it. Nothing adopts the new task yet; `mix precommit` is untouched. Signed-off-by: Luka Breitig <[email protected]>

  • Luka Breitig(13 Aug 26)

    chore(core): cap dialyzer schedulers in the precommit gate Dialyzer sizes its analysis worker pool to erlang:system_info(schedulers_online) (dialyzer_utils:parallelism/0, consumed by the regulator in dialyzer_coordinator), so its peak memory scales with the host's core count rather than with project size. On a 16-core machine that measured around 13G, enough for systemd-oomd to start killing unrelated desktop applications. Capping schedulers makes dialyzer use less; a memory cap alone only makes it thrash or get killed once it has already asked for too much. Measured effect: 4.1G peak instead of ~13G, on a from-scratch PLT build. The cap is keyed off the command rather than the step's display name, and set per step rather than for the whole run, so the suite in the same `mix precommit` keeps every core. MIX_DIALYZER_SCHEDULERS overrides it. Signed-off-by: Luka Breitig <[email protected]>

  • Luka Breitig(13 Aug 26)

    refactor(core): resolve the upload static mount at runtime Plug.Static's `from:` for /uploads read :upload_directory through Application.compile_env, pinning the value at compile time. config/test.exs suffixes that path with MIX_TEST_PARTITION so partitioned suites cannot write over each other, so every partition but the one the build was compiled for aborted at boot on a compile-env mismatch. That ruled out `mix test --partitions` entirely, which is the cheapest way to parallelise a suite whose sync phase dominates its runtime. Plug.Static.init/1 now runs on first use and its result is cached in :persistent_term, so the cost is paid once per node rather than per request. UploadStaticSecurity still gates the mount ahead of it, unchanged. Signed-off-by: Luka Breitig <[email protected]>

Tymeslot Website

Website

Open-Source Meeting Scheduling Software · Tymeslot

Eliminate scheduling back-and-forth with Tymeslot. Share your availability, let others book instantly, and sync with your calendar and video tools.

Redirects

Does not redirect

Security Checks

2 security checks failed (63 passed)

  • Domain Recently Created
  • Domain Very Recently Created

Server Details

  • IP Address94.16.119.60
  • Hostnamev2202503260852324142.megasrv.de
  • LocationNuremberg,Bayern,Germany,EU
  • ISPAnexia Cloud Solutions GmbH
  • ASNAS197540

Associated Countries

  • USUS
  • DEDE

Safety Score

Website marked as safe

100%

Blacklist Check

tymeslot.app was found on 0 blacklists

  • AntiSocial Blacklist
  • Artists Against 419
  • Badbitcoin
  • Bambenek Consulting
  • CERT Polska
  • CoinBlockerLists
  • CRDF
  • CryptoScamDB
  • EtherAddressLookup
  • EtherScamDB
  • Fake Website Buster
  • MetaMask EthPhishing
  • NABP Not Recommended Sites
  • OpenPhish
  • PetScams
  • PhishFeed
  • PhishFort
  • Phishing.Database
  • PhishStats
  • PhishTank
  • Phishunt
  • RPiList Not Serious
  • Scam.Directory
  • SecureReload Phishing List
  • Spam404
  • StopGunScams
  • Suspicious Hosting IP
  • ThreatFox
  • ThreatLog
  • TweetFeed
  • URLhaus
  • ViriBack C2 Tracker

Website Preview

Website preview

Tymeslot Reviews

More Scheduling

  • Open-source & self-hostable scheduling tool that helps you find the best date and time to meet. Create a poll with a few options, share the link, and let your participants vote on when they're available. No more back-and-forth emails.

About the Data: Tymeslot

Change History

Edit Tymeslot Data

You can edit Tymeslot's entry in this section of awesome-privacy.yml by submitting a PR to our GitHub repo.
Note that some of the information shown above has been aggregated from external sources, a list of these can be found data documentation.

Origin Data

Modify Data

API

You can access Tymeslot's data programmatically via our API. Simply make a GET request to:

https://api.awesome-privacy.xyz/v1/services/tymeslot

The REST API is free, no-auth and CORS-enabled. To learn more, view the API Docs or read the API Usage Guide.

Share Tymeslot

Help your friends compare Scheduling, and pick privacy-respecting software and services.
Share Tymeslot and Awesome Privacy with your network!