iv-org/invidious · 언어 Crystal · 웹 프레임워크 Kemal · DB PostgreSQL 14 · 렌더링 서버사이드 ECR · 라이선스 AGPLv3 · 버전 2.20260723.0(2026-07-23) · 분석 시점 2026-07-25 · shallow clone 소스 직접 분석)
Invidious는 한 문장으로 "YouTube 앞에 세우는 프라이버시 방패막이 웹서버"다. 보통 YouTube를 보면 당신의 브라우저가 구글 서버에 직접 접속하고, 그 순간 광고·쿠키·추적 스크립트·시청 로그가 모두 구글로 흘러 들어간다. Invidious는 그 사이에 중간 서버 하나를 세운다. 당신은 Invidious 인스턴스에만 접속하고, Invidious가 당신 대신 YouTube에서 영상 정보·댓글·자막을 긁어와 광고와 추적을 싹 걷어낸 깨끗한 페이지로 재조립해 준다.
YouTube를 직접 보는 건 백화점에 몸소 가는 것이다. 입장하는 순간 CCTV(추적)에 찍히고, 곳곳의 광고판(광고)을 지나야 하고, 회원카드(구글 로그인)로 구매 내역이 다 기록된다.
Invidious는 심부름꾼이다. 당신은 집(Invidious 서버)에서 "이 영상 가져와" 하고 시키기만 한다. 심부름꾼이 백화점(YouTube)에 대신 가서 물건만 받아 오므로, CCTV에 찍히는 건 당신이 아니라 심부름꾼이고, 광고판도 심부름꾼이 무시하고, 회원카드도 필요 없다. 게다가 이 심부름꾼은 자바스크립트를 한 줄도 안 요구해서, 아주 낡은 기기·브라우저에서도 가볍게 돈다.
중요한 구분이 하나 있다. Invidious는 영상을 저장·호스팅하지 않는다. 유튜브에서 실시간으로 긁어와 중계할 뿐이다. 그래서 "YouTube 다운로더"나 "불법 스트리밍 사이트"가 아니라, YouTube를 더 사생활 친화적으로 '보는 방법'이다. 실제로 README에도 "우리 도구의 사용 책임은 사용자에게 있으며, 각국의 정식 규정을 준수하라"는 면책 조항이 명시돼 있다.
Invidious는 새 프로젝트가 아니다(TrendShift 저장소 번호 6069로, 트렌딩 역사가 오래됐다). 그런데도 2026년 7월 다시 상위권에 오른 데는 이유가 있다. YouTube가 봇 차단을 강화할 때마다 Invidious 같은 프런트엔드가 "이번엔 살아남을까"라는 관심을 받고, 실제로 이 팀은 한 달에 여러 번 릴리스하며 끈질기게 대응하고 있다. 핵심 매력을 네 가지로 정리한다.
README가 내세우는 사용자 기능이 곧 매력의 핵심이다: 광고 없음, 추적 없음, 자바스크립트 불필요, 구글과 무관한 독립 구독, 모든 구독 채널 알림, 오디오 전용 모드(모바일 백그라운드 재생), Reddit 댓글 지원. 구글 계정 없이 구독·시청 기록을 관리하고, 그 데이터를 NewPipe·FreeTube로 내보내거나 가져올 수 있다.
Invidious는 페이지를 서버에서 완성해서(server-side rendering) 보낸다. 요즘 웹앱처럼 브라우저에서 자바스크립트로 화면을 그리는 게 아니라, 옛날 방식처럼 완성된 HTML을 보낸다. 그래서 자바스크립트를 꺼도 대부분 작동하고, 10년 된 노트북이나 텍스트 브라우저에서도 열린다. 배포물도 단일 정적 바이너리(static musl binary) 하나라 실행이 가볍다.
공개 인스턴스 목록(instances.invidious.io)에서 남의 서버를 골라 써도 되고, docker-compose로 내 서버에 직접 띄워 완전히 통제해도 된다. 라이선스가 AGPLv3이고 기여자 계약(CLA)이 없다는 점도 개발자 커뮤니티가 좋아하는 이유다 — 코드가 특정 회사에 종속되지 않는다는 신호이기 때문이다.
개발자에게 진짜 재미있는 건 YouTube의 방어 vs Invidious의 우회라는 끝없는 술래잡기다. 최근 릴리스 노트만 봐도 "YouTube 백엔드 변경으로 깨진 재생을 핫픽스", "YouTube User-Agent 업데이트" 같은 항목이 반복된다. 이 대응 과정에서 영상 스트림 획득이 아예 별도 서비스로 분리(§4)되는 큰 아키텍처 진화가 일어났다. "대규모 스크래핑을 지속가능하게 운영하는 법"의 살아 있는 교과서인 셈이다.
| 비교 | Invidious | NewPipe | FreeTube |
|---|---|---|---|
| 형태 | 셀프호스팅 웹서버 | 안드로이드 앱 | 데스크톱 앱(Electron) |
| 접근 | 브라우저로 어디서든 | 폰에 설치 | PC에 설치 |
| 여러 명 공유 | 가능(한 서버, 다수 사용자) | 1인용 | 1인용 |
| 언어/스택 | Crystal + PostgreSQL | Java/Kotlin | JS/Vue/Electron |
| 데이터 호환 | 서로 구독/기록을 import·export로 주고받음(같은 생태계) | ||
NewPipe·FreeTube가 "내 기기에 까는 개인용 앱"이라면, Invidious는 "서버에 세워 두고 URL로 나눠 쓰는 웹서비스"다. 그래서 Invidious는 혼자 쓸 수도, 친구·가족·커뮤니티에 인스턴스를 열어 줄 수도 있다. 셋은 경쟁이라기보다 구독 데이터를 주고받는 한 생태계다.
Invidious의 스택에서 가장 눈에 띄는 건 언어 선택이다. 대부분의 웹 프로젝트가 Python·JS·Go를 쓰는데, Invidious는 Crystal을 쓴다. Crystal은 "Ruby처럼 쓰기 쉬운데 C처럼 빠른(컴파일·정적 타입)" 언어를 목표로 한 마이너 언어다. 그 덕에 Invidious는 세상에서 제일 큰 Crystal 프로젝트가 됐고, 단일 정적 바이너리로 컴파일되어 배포가 가볍다.
| 구성요소 | 버전 | 역할 |
|---|---|---|
| Crystal | >=1.10 (Docker는 1.20.3) | 전체 언어. 정적 바이너리로 컴파일 |
| Kemal | 1.6.0 | 웹 프레임워크. Ruby의 Sinatra 같은 경량 라우팅/미들웨어 |
| pg (crystal-pg) | 0.28.0 | PostgreSQL 드라이버(주 DB 연결) |
| crystal-db | 0.13.1 | DB 추상화 계층(드라이버 공통 인터페이스) |
| sqlite3 | 0.21.0 | SQLite 드라이버(데이터 import/export 처리) |
| protodec | 0.1.5 | iv-org 자체 제작. YouTube continuation 토큰(protobuf) 인코딩/디코딩 |
| athena-negotiation | 0.1.1 | HTTP 콘텐츠 협상(Accept 헤더 처리) |
| http_proxy | 0.10.3 | 아웃바운드 HTTP 프록시 지원 |
| radix | 0.4.1 | Kemal이 쓰는 radix-tree 라우터 |
요즘 유행하는 React·Vue 같은 SPA 프레임워크가 전혀 없다. 화면은 ECR(Embedded Crystal) 템플릿으로 서버에서 HTML을 완성해 보낸다(PHP·ERB와 같은 옛 방식). 이게 "자바스크립트 불필요"라는 기능의 정체다. 플레이어만 video.js를 쓰고, 스타일은 순수 CSS다. 다국어는 59개 로케일 JSON(ar, de, en-US, es, fr, ja, ko, zh-* …)으로 지원하며 Weblate로 번역을 크라우드소싱한다.
Docker 멀티스테이지 빌드로 만든다. 특이하게도 빌드 과정에서 OpenSSL 3.6.3을 소스에서 직접 컴파일하는데, "Crystal이 OpenSSL과 상호작용하는 방식에 메모리 누수가 있어서"라는 주석이 붙어 있다(이슈 #1438). 런타임 이미지는 alpine:3.24, DB는 postgres:14 컨테이너를 함께 띄운다. systemd 유닛(invidious.service)과 쿠버네티스 매니페스트도 저장소에 들어 있다.
Invidious의 심장은 두 가지 설계 결정이다. 첫째, 공식 YouTube API를 안 쓴다. 대신 YouTube 앱이 내부적으로 서버와 대화할 때 쓰는 비공개 InnerTube 프로토콜을 흉내 낸다. 둘째, 영상 스트림 획득이 별도 서비스(invidious-companion)로 분리됐다. 아래 그림이 전체 흐름이다.
YouTube 데이터를 가져오는 코드의 심장은 src/invidious/yt_backend/youtube_api.cr다. Invidious는 https://www.youtube.com/youtubei/v1/*에 YouTube 앱인 척 POST를 보낸다. ClientType이라는 열거형에 흉내 낼 클라이언트 종류가 나열돼 있다: Web, Android, IOS, TvHtml5 등. 각각 진짜 앱의 이름·버전·User-Agent를 하드코딩해 둔다.
# 진짜 안드로이드 유튜브 앱 버전을 그대로 흉내
ANDROID_APP_VERSION = "21.29.366"
ANDROID_USER_AGENT = "com.google.android.youtube/#{ANDROID_APP_VERSION} ..."
IOS_APP_VERSION = "20.11.6"
ClientType::Web => { name: "WEB", version: "2.20260722.01.00", ... }
주요 엔드포인트 래퍼는 browse(채널·홈), next(연관영상·댓글), search, resolve_url, get_transcript다. 응답으로 온 원시 JSON을 videos/parser.cr, search/processors.cr 등이 해석한다. 페이지 넘김에 쓰는 continuation 토큰은 protobuf라서 자체 라이브러리 protodec로 만든다.
가장 중요한 최신 변화다. 영상의 실제 재생 URL을 얻는 player() 함수는 이제 YouTube를 직접 때리지 않고, 별도 서비스에 위임한다.
def player(video_id : String)
data = { "videoId" => video_id }
if CONFIG.invidious_companion.present?
# 컴패니언에게 "스트림 URL 좀 뽑아 줘" 위임
return self._post_invidious_companion("/youtubei/v1/player", data)
else
return nil # 컴패니언 없으면 재생 데이터가 없음!
end
end
즉 컴패니언을 붙이지 않으면 영상이 아예 재생되지 않는다. 왜 이렇게 나눴을까? YouTube가 스트림 URL에 서명(signature)·PoToken·nsig 같은 봇 차단 장치를 겹겹이 씌우는데, 이 "암호 풀기"가 워낙 자주 바뀌고 무거워서 본체에서 떼어내 독립 서비스(Deno/TypeScript)로 격리한 것이다. 본체는 COMPANION_POOL을 통해 컴패니언과 통신하고, Authorization: Bearer 토큰(16자 키)으로 인증한다.
가장 불안정한 조각(서명/토큰 해독)을 본체에서 분리하면, YouTube가 규칙을 바꿔도 컴패니언만 업데이트하면 된다. 본체(안정적인 웹·DB·렌더링)는 건드릴 필요가 없다. "변동성 높은 의존성을 경계 뒤로 밀어내는" 마이크로서비스 분리의 교과서적 사례다.
진입점 src/invidious.cr는 순서대로: 설정 로드(Config.load) → DB 연결(PG_DB, 실패 시 종료) → git 정보로 버전 계산 → 커넥션 풀 3개 생성(YT_POOL·GGPHT_POOL·COMPANION_POOL) → 테이블 무결성 검사 → 백그라운드 잡 시작 → 라우트 등록 → Kemal.run. 여기서 재밌는 건 버전 번호를 컴파일할 때 git 로그에서 뽑아 코드에 박는다는 점이다. 그래서 Dockerfile이 굳이 .git/ 폴더까지 복사한다.
src/invidious/jobs/의 워커들이 주기적으로 돈다. 설정에 따라 켜고 끈다.
| 잡 | 하는 일 |
|---|---|
| RefreshChannelsJob | 구독 채널의 새 업로드를 크롤링(channel_threads>0) |
| RefreshFeedsJob | 사용자 RSS 피드 갱신(feed_threads>0) |
| PullPopularVideosJob | 인기 영상 목록을 메모리에 캐시 |
| NotificationJob | Postgres LISTEN/NOTIFY로 새 영상 알림 전달 |
| SubscribeToFeedsJob | PubSubHubbub 구독(domain+hmac_key 필요) |
| ClearExpiredItemsJob | 만료된 nonce·세션 등 DB 청소 |
| StatisticsRefreshJob | /api/v1/stats용 통계 갱신 |
| InstanceListRefreshJob | 공개 인스턴스 목록 갱신 |
videos 테이블은 CREATE UNLOGGED TABLE로 선언돼 있다. UNLOGGED는 WAL(장애복구 로그)을 안 남기는 테이블이라 쓰기가 훨씬 빠르지만, 서버가 크래시하면 내용이 날아간다. Invidious는 영상 메타데이터를 "날아가도 다시 긁어오면 그만인 캐시"로 취급하기에, 안전성을 버리고 속도를 택했다. 목적에 맞는 영리한 트레이드오프다.
구조를 관통하는 규칙이 있다. "YouTube에서 긁어온다 → 해석한다 → 저장/렌더한다"의 3단계가 폴더로 나뉜다. yt_backend/가 긁어오고, videos/·channels/·search/·comments/가 원시 JSON을 우리 데이터로 파싱하고, database/가 캐시하고, frontend/·views/가 HTML로 렌더한다. 같은 데이터를 API로 내보낼 땐 jsonify/가 담당한다. 이 "수집·파싱·저장·표현" 분리가 대형 스크래퍼를 유지보수 가능하게 만드는 뼈대다.
Invidious는 "웹앱 하나"가 아니라 대규모 스크래핑·서버사이드 렌더링·마이너 언어·셀프호스팅이 한데 얽힌 종합 예제다. 기술별로 배울 거리를 뽑아 본다.
Crystal을 배우고 싶다면 이보다 큰 실사용 예제가 없다. Kemal의 라우팅·미들웨어 체인(압축 → API → 인증 → 프레임차단)이 웹 프레임워크의 뼈대를 아주 명료하게 보여 준다. Ruby/Sinatra를 알면 술술 읽히고, 정적 타입·파이버 동시성이라는 Crystal의 강점을 실전에서 관찰할 수 있다.
"공식 API가 없거나 부족할 때 어떻게 데이터를 얻는가"의 모범 답안이다. 클라이언트를 흉내 내고(hardcoded client) → 원시 JSON을 받아 → 별도 파서로 해석하는 3단 구조를 그대로 배울 수 있다. protobuf 토큰(protodec) 처리, User-Agent 위장, 커넥션 풀링까지 실전 스크래핑의 정수가 담겨 있다.
InnerTube 흉내 기법은 강력하지만, 대상 서비스의 약관·부하·법적 경계를 항상 의식해야 한다. Invidious도 README에 면책 조항을 두고 "각국 규정을 준수하라"고 명시한다. 학습 목적으로 원리를 이해하되, 무단 대량 요청으로 남의 서비스에 부담을 주지 않는 선을 지키자.
React에 익숙한 사람에게 Invidious의 .ecr 템플릿은 신선한 역발상이다. "화면을 서버에서 완성해 보낸다"는 옛 방식이 경량·접근성·프라이버시(JS 불필요) 측면에서 왜 여전히 강력한지 체감할 수 있다. 최신 웹이 다시 SSR로 회귀하는 흐름(Astro·HTMX 등)의 원형을 여기서 본다.
UNLOGGED 테이블로 "안전성 대신 속도"를 택한 결정, 인기 영상을 메모리에 캐시하는 잡, git 로그로 버전을 컴파일할 때 코드에 박는 매크로 — 이 세 가지는 각각 "캐시 설계", "백그라운드 워커", "메타프로그래밍"의 좋은 미니 사례다.
본체 + 컴패니언 분리는 "자주 바뀌는 의존성을 경계 뒤로 밀어내는" 실전 설계다. 왜 굳이 스트림만 떼어냈는지, 그 경계에서 인증(Bearer 토큰)과 통신(커넥션 풀)이 어떻게 이뤄지는지 따라가 보면, 마이크로서비스 분리 판단의 감을 익힐 수 있다.
· 미니 프런트엔드: 아무 공개 API(예: Hacker News)를 골라, 서버에서 JSON을 받아 순수 HTML로 렌더하는 초경량 페이지를 만들어 본다(JS 0줄 목표).
· 클라이언트 흉내: 어떤 앱의 네트워크 요청을 개발자도구로 관찰해 User-Agent·헤더를 재현하고, 같은 응답을 받아 본다(합법적 공개 API로).
· UNLOGGED 실험: PostgreSQL에서 일반 테이블 vs UNLOGGED 테이블의 대량 INSERT 속도를 직접 재 본다.
저장소 자체에는 구체적 사양(RAM/CPU 수치)이 없다(공식 사양은 docs.invidious.io에 있다). 하지만 코드에서 성격을 읽을 수 있다.
| 구분 | 성격 | 근거 |
|---|---|---|
| 런타임(운영) | 가벼움 | 단일 정적 바이너리 + alpine:3.24, 비루트(uid 1000) 실행. README도 "Lightweight"를 첫 기능으로 강조 |
| 필수 동반 서비스 | PostgreSQL 14 | 영속 볼륨(postgresdata)이 붙는 DB 컨테이너 필요 |
| 필수 동반 서비스 2 | invidious-companion | 없으면 영상 재생 불가(§4). 별도 컨테이너·리소스 필요 |
| 빌드(컴파일) | 무거움 | 플레이어 의존성 fetch "20분 이상", OpenSSL 소스 컴파일, crystal spec 전체 실행 |
| 동시성 | 단일 프로세스 | 멀티스레드(MT) 기본 꺼짐("아직 불안정"). Crystal 파이버로 동시성 처리 |
Invidious는 "만드는 데는 손이 많이 가지만, 완성품은 깃털처럼 가벼운" 부류다. 처음 도커 이미지를 빌드할 땐 시간·CPU가 꽤 들지만(플레이어 의존성 + OpenSSL + 테스트), 일단 뜨고 나면 낡은 VPS에서도 잘 돈다. 다만 혼자 두면 재생이 안 되고, PostgreSQL과 컴패니언을 반드시 곁에 둬야 한다는 점을 기억하자.
instances.invidious.io에서 인스턴스 하나를 골라 아무 영상을 연다. 브라우저 자바스크립트를 끄고도 페이지가 뜨는지 확인하고, 개발자도구 Network 탭에서 요청이 구글이 아니라 인스턴스로만 가는지 관찰한다.
저장소를 클론해 docker-compose up으로 invidious + postgres를 띄운다. config.example.yml을 열어 hmac_key를 pwgen 20 1로 새로 만들고, 재생을 위해 invidious-companion을 붙여 본다. "컴패니언 없으면 재생 안 됨"을 직접 확인.
/api/v1/videos/{id}, /api/v1/search?q=...를 직접 호출해 응답 JSON 구조를 분석한다. 그 다음 코드에서 routes/api/v1/videos.cr → jsonify/api_v1/로 이어지는 직렬화 경로를 따라가 본다. 설정에서 disable_abusable_api: true로 이 API를 꺼 보고 동작 변화도 확인.
yt_backend/youtube_api.cr에서 ClientType·HARDCODED_CLIENTS를 읽고, 검색 한 번이 search → _post_json → youtubei/v1/search로 어떻게 흘러가는지 추적한다. User-Agent를 바꾸면 응답이 어떻게 달라지는지 실험(합법적 범위 내에서). continuation 토큰이 protodec로 어떻게 만들어지는지도 살펴본다.
새 로케일을 locales/에 추가하거나, routes/에 아주 간단한 새 라우트(예: /hello)를 만들어 routing.cr에 등록하고 crystal build로 컴파일해 본다. 나아가 videos 테이블을 LOGGED로 바꿔 성능·복구 동작 차이를 직접 측정해 UNLOGGED 설계의 의미를 체감한다.
| 주차 | 주제 | 구체적으로 |
|---|---|---|
| 1주차 | Crystal + Kemal 기초 | Crystal 문법(Ruby 유사), 파이버 동시성, Kemal로 초미니 웹서버 만들기. Invidious의 routing.cr 읽기 |
| 2주차 | 웹 스크래핑 & 비공개 API | InnerTube 개념, 클라이언트 흉내, User-Agent/헤더, protobuf 토큰. yt_backend/ 정독 |
| 3주차 | PostgreSQL & 캐시 설계 | crystal-db, UNLOGGED 테이블, LISTEN/NOTIFY, 배열 컬럼(subscriptions/watched). database/ + config/sql/ |
| 4주차 | Docker 멀티스테이지 & 셀프호스팅 | 정적 musl 빌드, 멀티스테이지, docker-compose, 리버스 프록시(external_port), 컴패니언 연동 |
| 5주차 | 프라이버시 프런트엔드 생태계 | NewPipe·FreeTube·Piped 비교, import/export 호환, AGPLv3 함의, "고양이-쥐" 대응 전략 이해 |
관심사에 따라 두 갈래로 파면 좋다. "웹 개발·SSR"이 궁금하면 Crystal→Kemal→ECR 템플릿→라우팅 축으로, "데이터 수집·인프라"가 궁금하면 InnerTube→파싱→PostgreSQL 캐시→Docker/컴패니언 축으로 파면 된다. 둘 다 config.example.yml(27KB 주석)과 CHANGELOG.md가 최고의 안내서다 — 설정 하나하나에 "왜 이게 필요한지"가 적혀 있고, 체인지로그엔 "무엇이 왜 깨졌고 어떻게 고쳤는지"가 쌓여 있다.
| 용어 | 뜻 |
|---|---|
| Invidious | YouTube의 오픈소스 대체 프런트엔드. 광고·추적·JS 없이 서버가 대신 유튜브를 긁어와 보여 줌 |
| 대체 프런트엔드 | 원본 서비스의 화면만 제3자가 새로 만든 것. 프라이버시·경량·오픈소스 지향(Nitter·Redlib 등) |
| Crystal | Ruby 문법 + C 성능을 노린 컴파일·정적타입 언어. Invidious가 세계 최대 실사용 예제 |
| Kemal | Crystal용 경량 웹 프레임워크. Ruby의 Sinatra 격. radix-tree 라우팅 + 미들웨어 체인 |
| InnerTube | YouTube 앱·웹이 쓰는 비공개 내부 API. 공식 Data API와 다름. Invidious가 이걸 흉내 냄 |
| ClientType | 흉내 낼 클라이언트 종류 열거형(Web·Android·IOS·TvHtml5…). 각기 앱 버전·UA 하드코딩 |
| invidious-companion | 영상 스트림 URL을 뽑아 오는 별도 서비스(Deno/TS). 없으면 재생 불가. 서명/PoToken 해독 격리 |
| 서명 / PoToken / nsig | YouTube가 스트림 URL에 씌우는 봇 차단 장치. 자주 바뀌어 컴패니언으로 분리 대응 |
| protodec | iv-org 자체 Crystal 라이브러리. continuation 토큰(protobuf) 인코딩/디코딩 |
| continuation 토큰 | "다음 페이지"를 가리키는 YouTube의 protobuf 토큰(무한 스크롤·페이지네이션) |
| ECR | Embedded Crystal 템플릿. 서버에서 HTML을 완성해 보냄(ERB·PHP 방식). "JS 불필요"의 정체 |
| SSR | Server-Side Rendering. 화면을 서버가 그려서 완성 HTML로 전송. 경량·접근성·프라이버시 이점 |
| UNLOGGED 테이블 | WAL을 안 남겨 빠르지만 크래시 시 소실되는 Postgres 테이블. videos를 '캐시'로 취급 |
| 커넥션 풀 | 재사용 가능한 HTTP 연결 묶음. YT_POOL·GGPHT_POOL·COMPANION_POOL 3종 |
| HMAC 키 | CSRF 토큰·쿠키·PubSub 검증에 쓰는 필수 비밀키. pwgen으로 무작위 생성 |
| PubSubHubbub | 새 영상을 웹훅으로 즉시 통지받는 구독 프로토콜(SubscribeToFeedsJob) |
| 정적 musl 바이너리 | 의존성을 다 넣어 컴파일한 단일 실행파일. "복사만 하면 어디서든 실행" |
| Weblate | 번역 크라우드소싱 플랫폼. Invidious의 59개 로케일이 여기서 관리됨 |
| AGPLv3 | 네트워크로 제공해도 소스 공개 의무가 있는 강한 카피레프트 라이선스. 수정 시 소스 URL 공개 필요 |
| 날짜 기반 버전 | semver 대신 2.YYYYMMDD.micro 형식(예: 2.20260723.0). 롤링 릴리스 |
src/invidious.cr(부팅) · src/invidious/routing.cr(라우트) · src/invidious/yt_backend/youtube_api.cr(★InnerTube 코어) · src/invidious/routes/companion.cr(스트림 프록시) · config/config.example.yml(설정)